<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<style>
	body {
		margin: 0;
		padding: 4px 8px 24px;
		background-color: #ffffff;
		overflow-y: scroll
	}
	h1 {
		margin: 5px 0 0 0;
		font-size: 18px;
		font-weight: normal;
		text-align: center
	}
	header {
		margin: -24px 0 5px 0;
		line-height: 24px
	}
	button {
		font: 12px sans-serif;
		cursor: pointer
	}
	p {
		margin: 5px 0 5px 0
	}
	a {
		color: #0366d6
	}
	#hl {
		position: absolute;
		display: none;
		overflow: hidden;
		white-space: nowrap;
		pointer-events: none;
		background-color: #ffffe0;
		outline: 1px solid #ffc000;
		height: 15px
	}
	#preview_wrapper {
		position: absolute;
		display: none;
		background-color: #ffffe0;
		outline: 1px solid #ffc000;
		pointer-events: none;
	}
	#hl span {
		padding: 0 3px 0 3px
	}
	#status {
		overflow: hidden;
		white-space: nowrap
	}
	#match {
		overflow: hidden;
		white-space: nowrap;
		display: none;
		float: right;
		text-align: right
	}
	#reset {
		cursor: pointer
	}
	#canvas {
		width: 100%;
		height: 300px;
	}
	.selectionPart {
		position: absolute;
		display: none;
		pointer-events: none;
		box-sizing: border-box;
	}
	.sel {
		background-color: #dada0040
	}
	.act {
		background-color: #00da0040
	}
	.dif {
		background-color: #00dada40
	}
	.top {
		border-top: .1px solid #000
	}
	.left {
		border-left: .1px solid #000
	}
	.bottom {
		border-bottom: .1px solid #000
	}
	.right {
		border-right: .1px solid #000
	}
	#info-tooltip {
		display: none;
		position: absolute;
		top: 10px;
		right: 48px;
		width: 204px;
		border: 1px solid #666666;
		background: #ffffe0;
		border-radius: 8px;
		padding: 4px
	}
	#heatmap-height-line {
		text-decoration: #0366d6 dashed underline;
		cursor: pointer;
		font-family: monospace
	}
	#heatmap-info {
		cursor: help
	}
	#heatmap-info:hover ~ #info-tooltip {
		display: block
	}
	.toolbarIcon {
		width: 24px;
		height: 24px;
		margin-top: 4px;
		margin-bottom: 4px;
		display: flex;
		justify-content: center;
		align-items: center;
	}
	.toolbarSelected {
		background-color: #cccccc;
		outline: 1px solid #999999;
		border-radius: 4px;
	}
	.toolbarIcon:hover {
		background-color: #ffffe0;
		outline: 1px solid #ffc000;
		border-radius: 4px;
	}
	.bordered {
		outline: 1px solid #999999;
	}
	.colortip {
		width: 100px;
		box-sizing: border-box;
		padding: 4px;
		background: linear-gradient(var(--c1), var(--c2))
	}
	.status {
		position: fixed;
		bottom: 0;
		padding: 2px 4px;
		background-color: #ffffe0;
		border-top: 1px solid #ffc000;
		border-left: 1px solid #ffc000;
		border-right: 1px solid #ffc000;
	}
</style>
</head>
<body style='font: 12px Verdana, sans-serif'>
<div class='bordered' style="display: flex">
	<div class='bordered' style="width: 20px; margin-right: 1px; padding: 2px; float: left; background-color: #efefef; writing-mode: vertical-rl; transform: scale(-1); text-align: center; user-select: none">
		<pre id="heatmap-height-line"> 1 sec : 20 ms </pre>
	</div>

	<div id='heatmap-canvas-container' style="width: 100%; position: relative" autofocus>
		<div id='heatmap-canvas-wrapper' style='overflow: hidden; width: 100%; font-size: 0; padding-bottom: 1px'>
			<canvas id='heatmap-canvas' style='height: 314px'></canvas>
		</div>

		<div id='middleActive' class='selectionPart top bottom act'><span></span></div>
		<div id='leftActive' class='selectionPart top left bottom act'><span></span></div>
		<div id='rightActive' class='selectionPart top right bottom act'><span></span></div>
		<div id='leftMiddleActive' class='selectionPart right'><span></span></div>
		<div id='rightMiddleActive' class='selectionPart left'><span></span></div>

		<div id='middleSelection' class='selectionPart top bottom sel'><span></span></div>
		<div id='leftSelection' class='selectionPart top left bottom sel'><span></span></div>
		<div id='rightSelection' class='selectionPart top right bottom sel'><span></span></div>
		<div id='leftMiddleSelection' class='selectionPart right'><span></span></div>
		<div id='rightMiddleSelection' class='selectionPart left'><span></span></div>

		<div id='middleDiff' class='selectionPart top bottom dif'><span></span></div>
		<div id='leftDiff' class='selectionPart top left bottom dif'><span></span></div>
		<div id='rightDiff' class='selectionPart top right bottom dif'><span></span></div>
		<div id='leftMiddleDiff' class='selectionPart right'><span></span></div>
		<div id='rightMiddleDiff' class='selectionPart left'><span></span></div>
	</div>

	<div class='bordered' style="user-select: none; background-color: #efefef; padding: 2px; margin-left: 1px">
		<div id="heatmap-info" class="toolbarIcon"><svg height="20px" width="20px" viewBox="0 0 392.598 392.598"><path style="fill:#FFFFFF;" d="M274.457,148.234c0.194,17.131-5.236,33.358-15.515,46.933c-23.984,32-36.784,68.331-36.784,104.921 v1.164h-51.911c-0.065-37.107-12.929-73.956-37.107-106.537c-12.735-15.386-17.455-32.065-15.321-49.842 c5.624-45.834,39.822-70.917,74.343-74.925C237.026,67.879,274.457,103.693,274.457,148.234z"></path><rect style="fill:#40b2ff;" x="170.311" y="323.038" width="51.846" height="47.774"></rect><path style="fill:#ff8d40;" d="M196.299,91.604c-0.905,0-1.939,0-2.844,0.065c-17.842,0.84-33.681,10.279-43.442,24.178h42.408 c6.012,0,10.925,4.848,10.925,10.925s-4.848,10.925-10.925,10.925h-51.588c-0.453,2.651-0.84,5.301-1.034,8.016 c-0.259,4.719,0.129,9.244,1.099,13.77h25.988c6.012,0,10.925,4.848,10.925,10.925c0,6.012-4.848,10.925-10.925,10.925h-16.356 c0.065,0.129,0.129,0.323,0.323,0.388c22.238,30.061,35.943,63.418,40.016,97.681h10.796c3.943-34.263,17.519-67.556,39.952-97.358 c7.37-9.826,11.313-21.463,11.313-33.875C252.865,117.075,227.523,91.604,196.299,91.604z"></path><path style="fill:#004d80;" d="M196.299,36.137c6.012,0,10.925-4.848,10.925-10.925V10.925C207.224,4.913,202.376,0,196.299,0 c-6.012,0-10.925,4.848-10.925,10.925v14.287C185.374,31.289,190.287,36.137,196.299,36.137z"></path> <path style="fill:#004d80;" d="M333.543,137.309h-14.287c-6.012,0-10.925,4.848-10.925,10.925c0,6.012,4.849,10.925,10.925,10.925 h14.287c6.012,0,10.925-4.848,10.925-10.925S339.556,137.309,333.543,137.309z"></path> <path style="fill:#004d80;" d="M73.277,137.309H59.055c-6.012,0-10.925,4.848-10.925,10.925c0,6.012,4.848,10.925,10.925,10.925 h14.287c6.012,0,10.925-4.848,10.925-10.925C84.202,142.158,79.289,137.309,73.277,137.309z"></path> <path style="fill:#004d80;" d="M285.64,43.507l-10.15,10.149c-4.267,4.267-4.267,11.119,0,15.45c4.267,4.267,11.119,4.267,15.451,0 l10.15-10.149c4.267-4.267,4.267-11.119,0-15.451C296.824,39.176,289.907,39.176,285.64,43.507z"></path> <path style="fill:#004d80;" d="M101.657,227.491l-10.085,10.02c-4.267,4.267-4.267,11.119,0,15.451 c4.267,4.331,11.119,4.267,15.451,0l10.02-10.02c4.267-4.267,4.267-11.119,0-15.451 C112.776,223.224,105.859,223.224,101.657,227.491z"></path> <path style="fill:#004d80;" d="M290.941,227.491c-4.267-4.267-11.119-4.267-15.451,0c-4.267,4.267-4.267,11.119,0,15.451 l10.15,10.149c4.267,4.267,11.119,4.267,15.451,0s4.267-11.119,0-15.451L290.941,227.491z"></path> <path style="fill:#004d80;" d="M101.657,68.978c4.267,4.267,11.119,4.267,15.451,0c4.267-4.267,4.267-11.119,0-15.451l-10.02-10.02 c-4.267-4.267-11.119-4.267-15.451,0c-4.267,4.267-4.267,11.119,0,15.451L101.657,68.978z"></path> <path style="fill:#004d80;" d="M191.257,48.097c-51.2,2.457-92.962,44.606-95.095,95.806c-1.034,23.208,5.818,45.188,19.523,63.741 c21.463,28.962,32.84,61.414,32.84,93.996v80.032c0,6.012,4.848,10.925,10.925,10.925h73.632c6.012,0,10.925-4.848,10.925-10.925 l0.065-81.584c0-31.935,11.184-63.677,32.388-91.798c13.123-17.455,20.04-38.141,20.04-60.057 C296.436,91.345,248.921,45.253,191.257,48.097z M222.093,370.812h-51.846v-47.774h51.846L222.093,370.812L222.093,370.812z M258.941,195.168c-23.984,32-36.784,68.331-36.784,104.921v1.164h-51.911c-0.065-37.107-12.929-73.956-37.107-106.537 c-12.735-15.386-17.455-32.065-15.321-49.842c5.624-45.834,39.822-70.917,74.343-74.925c44.865-2.069,82.295,33.745,82.295,78.287 C274.651,165.366,269.22,181.592,258.941,195.168z"></path></svg></div>
		<div id="search" class="toolbarIcon" title="Search by Regex [Ctrl + F]"><svg height="20px" width="20px" viewBox="-39.26 -39.26 471.13 471.13" xml:space="preserve" fill="#000000" stroke="#000000" stroke-width="0.00392609"><path style="fill:#FFFFFF;" d="M273.713,147.774c0,69.495-56.501,125.931-125.931,125.931c-69.495,0-125.931-56.501-125.931-125.931 c0-69.495,56.501-125.931,125.931-125.931C217.212,21.843,273.713,78.344,273.713,147.774z"></path><path style="fill:#40b2ff;" d="M370.747,348.695c0,1.422-1.616,6.271-7.176,12.283c-6.206,6.659-12.477,9.762-14.739,9.762h-0.065 c-19.459-1.616-61.996-43.184-109.576-106.796c0-0.065,0-0.065,0-0.129c9.18-7.24,17.455-15.515,24.63-24.63 c0.065,0,0.065,0,0.129,0C327.564,286.699,369.131,329.301,370.747,348.695z"></path><path style="fill:#ff8d40;" d="M208.679,86.877l-14.481,14.481c-17.131,17.131-46.545,5.042-46.545-19.265V61.6 c-49.002,0-88.372,40.792-86.109,90.246c2.004,43.895,38.141,80.032,82.036,82.036c49.455,2.263,90.246-37.172,90.246-86.109 C233.956,124.049,224.323,102.522,208.679,86.877z"></path><path style="fill:#004d80;" d="M276.105,220.954c12.347-21.527,19.459-46.545,19.459-73.18c0-81.455-66.327-147.717-147.846-147.717 S0,66.32,0,147.904s66.327,147.717,147.717,147.717c26.57,0,51.523-7.111,73.18-19.459 c39.822,53.333,91.863,113.519,126.061,116.364c12.283,0.517,22.885-6.723,32.776-16.743c5.172-5.624,13.77-16.937,12.8-28.768 C389.624,312.881,329.438,260.776,276.105,220.954z M147.717,273.77c-69.495,0-125.931-56.501-125.931-125.931 S78.287,21.908,147.717,21.908s125.931,56.501,125.931,125.931S217.212,273.77,147.717,273.77z M363.572,360.978 c-6.206,6.659-12.412,9.762-14.739,9.762h-0.065c-19.459-1.616-61.996-43.184-109.576-106.796c0-0.065,0-0.065,0-0.129 c9.18-7.24,17.455-15.515,24.63-24.63c0.065,0,0.065,0,0.129,0c63.612,47.58,105.18,90.182,106.796,109.576 C370.747,350.118,369.131,354.966,363.572,360.978z"></path></svg></div>
		<div id="filter" class="toolbarIcon" title="Filter by Regex [Shift + Ctrl + F]"><svg height="16px" width="16px" viewBox="0 0 392.541 392.541"><path style="fill:#FFFFFF;" d="M219.345,344.911V190.988c0-2.909,1.164-5.689,3.232-7.758L367.386,39.586 c3.814-3.814,3.426-7.434,2.392-9.891c-1.487-3.62-6.012-7.887-13.834-7.887H36.137c-7.822,0-12.347,4.267-13.834,7.887 c-1.034,2.392-1.422,6.077,2.392,9.891L169.503,183.23c2.069,2.004,3.232,4.848,3.232,7.758v174.093L219.345,344.911z"></path><path style="fill:#40b2ff;" d="M59.733,43.594L91.41,75.012c0.065,0,0.259,0,0.323,0h97.228c6.012,0,10.925,4.848,10.925,10.925 c0,6.012-4.848,10.925-10.925,10.925h-75.636l22.044,21.786h31.741c6.012,0,10.925,4.848,10.925,10.925 c0,6.012-4.848,10.925-10.925,10.925h-9.826l38.853,38.335l136.21-135.176L59.733,43.594L59.733,43.594z"></path><path style="fill:#004d80;" d="M356.008,0.022H36.137c-30.578,0-47.968,33.487-26.828,55.079l141.705,140.412v186.053 c0.065,7.111,7.564,13.382,15.192,10.02l68.396-29.543c4.008-1.681,6.594-5.624,6.594-10.02V195.513l141.64-140.412 C404.234,33.767,388.008,0.022,356.008,0.022z M367.45,39.586L222.642,183.23c-2.069,2.004-3.232,4.848-3.232,7.758v153.923 l-46.61,20.105V190.988c0-2.909-1.164-5.689-3.232-7.758L24.759,39.586c-8.339-8.792,1.099-18.295,11.442-17.778h319.806 C367.45,21.485,375.337,32.41,367.45,39.586z"></path></svg></div>
		<div id="flame-diff-reverse" class="toolbarIcon" title="Reverse Diff [Ctrl + R]"><svg height="16px" width="16px" viewBox="0 0 392.598 392.598"><path style="fill:#004d80;" d="M382.488,286.384c-3.62-3.62-9.438-4.267-13.77-1.422c-30.19,20.105-65.552,30.707-102.077,30.707 c-45.64,0-88.76-16.485-122.634-46.61l23.725-23.725c7.37-8.21,1.487-18.166-7.758-18.618H40.96 c-6.012,0-10.925,4.848-10.925,10.925v118.95c0.517,10.279,12.024,14.222,18.618,7.758l24.76-24.76 c37.947,34.263,86.626,53.01,138.085,53.01c55.143,0,125.285-23.014,172.154-92.444 C386.69,295.822,386.108,290.069,382.488,286.384z M211.692,370.747c-49.325,0-95.741-19.265-130.586-54.109 c-4.073-4.073-11.378-4.073-15.451,0L51.95,330.343v-81.842h81.778l-13.059,13.059c-4.202,4.59-4.461,10.99,0,15.451 c39.046,39.047,90.893,60.509,146.036,60.509c21.85,0,43.378-3.426,63.806-10.02C297.219,355.426,255.651,370.747,211.692,370.747z "></path><path style="fill:#004d80;" d="M248.605,123.475L224.88,147.2c-7.37,8.21-1.487,18.166,7.758,18.618h119.014 c6.012,0,10.925-4.848,10.925-10.925V35.943c-0.517-10.279-12.024-14.222-18.618-7.758l-24.76,24.76 C281.122,18.683,232.508,0,180.985,0C125.841,0,55.829,23.014,8.831,92.444c-2.909,4.331-2.327,10.02,1.422,13.77 c3.62,3.62,9.438,4.267,13.77,1.422C54.213,87.531,89.575,76.929,126.1,76.929C171.611,76.865,214.795,93.349,248.605,123.475z M180.92,21.786c49.325,0,95.741,19.265,130.586,54.109c4.073,4.073,11.378,4.073,15.451,0l13.705-13.705v81.778h-81.778 l13.059-13.059c4.202-4.59,4.461-10.99,0-15.451c-39.046-39.046-90.893-60.509-146.036-60.509c-21.851,0-43.378,3.426-63.806,10.02 C95.393,37.107,136.96,21.786,180.92,21.786z"></path><path style="fill:#ff8d40;" d="M340.661,62.255L326.956,75.96c-4.073,4.073-11.378,4.073-15.451,0 c-34.844-34.844-81.261-54.109-130.586-54.109c-43.96,0-85.592,15.192-118.82,43.248c20.428-6.659,41.891-10.02,63.806-10.02 c55.143,0,107.055,21.463,146.036,60.509c4.461,4.396,4.202,10.796,0,15.451l-13.059,12.994h81.778L340.661,62.255L340.661,62.255z"></path><path style="fill:#40b2ff;" d="M81.041,316.638c34.844,34.844,81.261,54.109,130.586,54.109c43.96,0,85.592-15.192,118.82-43.249 c-20.428,6.659-41.891,10.02-63.806,10.02c-55.143,0-107.055-21.463-146.036-60.509c-4.461-4.396-4.202-10.796,0-15.451 l13.059-13.059H51.95v81.778l13.705-13.705C69.728,312.566,76.968,312.566,81.041,316.638z"></path></svg></div>

		<div id="flame-mode-flame" class="toolbarIcon toolbarSelected" style="margin-top: 16px" title="FlameGraph [Ctrl + 1]">
			<svg height="20px" width="20px" viewBox="0 0 20 20"><rect x="0" y="0" style="fill:#ff8d40;" width="20" height="4"></rect><rect x="0" y="5" style="fill:#40b2ff;" width="10" height="4"></rect><rect x="11" y="5" style="fill:#ff8d40;" width="8" height="4"></rect><rect x="0" y="10" style="fill:#40b2ff;" width="8" height="4"></rect><rect x="11" y="10" style="fill:#ff8d40;" width="4" height="4"></rect><rect x="0" y="15" style="fill:#40b2ff;" width="4" height="4"></rect><rect x="5" y="15" style="fill:#ff8d40;" width="2" height="4"></rect></svg>
		</div>
		<div id="flame-mode-reversed" class="toolbarIcon" title="Reversed FlameGraph [Ctrl + 2]">
			<svg height="20px" width="20px" viewBox="0 0 20 20"><rect x="0" y="0" style="fill:#ff8d40;" width="6" height="4"></rect><rect x="7" y="0" style="fill:#ff8d40;" width="5" height="4"></rect><rect x="13" y="0" style="fill:#ff8d40;" width="4" height="4"></rect><rect x="18" y="0" style="fill:#ff8d40;" width="2" height="4"></rect><rect x="0" y="5" style="fill:#ff8d40;" width="6" height="4"></rect><rect x="7" y="5" style="fill:#40b2ff;" width="5" height="4"></rect><rect x="13" y="5" style="fill:#ff8d40;" width="4" height="4"></rect><rect x="18" y="5" style="fill:#40b2ff;" width="2" height="4"></rect><rect x="0" y="10" style="fill:#ff8d40;" width="4" height="4"></rect><rect x="7" y="10" style="fill:#40b2ff;" width="2" height="4"></rect><rect x="13" y="10" style="fill:#40b2ff;" width="4" height="4"></rect><rect x="13" y="15" style="fill:#ff8d40;" width="2" height="4"></rect></svg>
		</div>
		<div id="flame-mode-methods" class="toolbarIcon" title="Methods List [Ctrl + 3]">
			<svg height="20px" width="20px" viewBox="0 0 20 20"><rect x="0" y="0" style="fill:#ff8d40;" width="20" height="4"></rect><rect x="0" y="5" style="fill:#40b2ff;" width="16" height="4"></rect><rect x="0" y="10" style="fill:#ff8d40;" width="8" height="4"></rect><rect x="0" y="15" style="fill:#40b2ff;" width="4" height="4"></rect></svg>
		</div>

		<div id="info-tooltip">
			<div style="float:left">
				<div style="padding: 4px;">Frame types:</div>
				<div class="colortip" style="--c1:#b2e1b2;--c2:#c6f5c6">Interpreted</div>
				<div class="colortip" style="--c1:#50e150;--c2:#6eff6e">JIT-Compiled</div>
				<div class="colortip" style="--c1:#50cccc;--c2:#6eeaea">Inlined</div>
				<div class="colortip" style="--c1:#e15a5a;--c2:#ff8282">Native</div>
				<div class="colortip" style="--c1:#c8c83c;--c2:#e6e646">C++</div>
				<div class="colortip" style="--c1:#e17d00;--c2:#ff9b00">Kernel</div>
				<div class="colortip" style="--c1:#cce880;--c2:#e0fc94">C1-Compiled</div>
			</div>
			<div style="float:left; margin-left: 4px">
				<div style="padding: 4px;">Special colors:</div>
				<div class="colortip" style="--c1:#40b2ff;--c2:#40b2ff">Matches regexp</div>
				<div class="colortip" style="--c1:#ffdd33;--c2:#ffdd33">New frames</div>
				<div class="colortip" style="--c1:#66e0f0;--c2:#66e0f0">Same frames</div>
				<div class="colortip" style="--c1:#42ff8e;--c2:#ccffe0">Less frames</div>
				<div class="colortip" style="--c1:#ff8d40;--c2:#ffe0cc">More frames</div>
			</div>
			<div style="float:left; margin-top: 4px; width: 100%; border: 1px solid #666666; border-radius: 2px; padding: 2px; box-sizing: border-box;">
				Shift + Click - Select range<br>
				Ctrl + Click - Compare<br>
				<br>
				Ctrl + F - Search<br>
				Shift + Ctrl + F - Filter<br>
				Ctrl + R - Switch Diff<br>
				<br>
				Ctrl + 1 - Select FlameGraph mode<br>
				Ctrl + 2 - Select Reversed FlameGraph mode<br>
				Ctrl + 3 - Select Methods List mode<br>
			</div>
		</div>
	</div>
</div>
<div style="display:none">
	<pre id="executionsHeatmap">/*executionsHeatmap:*/</pre>
</div>
<div style="display:none">
	<pre id="methods">/*methods:*/</pre>
</div>
<span class="status" style="border-right: none; border-top-left-radius: 4px; right:0;">
	<span id="produced">Produced by <a href='https://github.com/async-profiler/async-profiler'>async-profiler</a>&nbsp;</span>
	<span id='match' style="display: none">Matched: <span id='matchval'></span>&nbsp;<span id='reset' title='Clear'>&#x274c;</span></span>
</span>
<h1 id="title">/*title:*/</h1>
<canvas id='canvas'></canvas>
<div id='hl'><span></span></div>
<div id='preview_wrapper'><canvas id='preview'></canvas></div>

<span id='status' class="status" style="border-left: none; border-top-right-radius: 4px; display: none; left:0;">&nbsp;</span>
<script>
	// Copyright The async-profiler authors
	// SPDX-License-Identifier: Apache-2.0
	'use strict';

	let dR, dG, dB;
	let dRs, dGs, dBs;
	let dRdg, dGdg, dBdg;
	let dRdl, dGdl, dBdl;

	function setH(H) {
		const dX = Math.round(256 - Math.abs(H % 2 - 1) * 256);
		switch (H | 0) {
			case 0:
				dR = 256; dG = dX; dB = 0;
				break;
			case 1:
				dR = dX; dG = 256; dB = 0;
				break;
			case 2:
				dR = 0; dG = 256; dB = dX;
				break;
			case 3:
				dR = 0; dG = dX; dB = 256;
				break;
			case 4:
				dR = dX; dG = 0; dB = 256;
				break;
			case 5:
				dR = 256; dG = 0; dB = dX;
				break;
		}
	}

	setH(0.4);

	dRdg = dR;
	dGdg = dG;
	dBdg = dB;

	setH(2.4);

	dRdl = dR;
	dGdl = dG;
	dBdl = dB;

	setH(3.4);

	dRs = dR;
	dGs = dG;
	dBs = dB;

	setH(0.4);

	let root, rootLevel, px, pattern;
	let showFound = false;
	let filterFrames = false;
	const levels = [];

	const canvas = document.getElementById('canvas');
	let c = canvas.getContext('2d');
	const hl = document.getElementById('hl');
	const preview_wrapper = document.getElementById('preview_wrapper')
	const status = document.getElementById('status');

	const canvasWidth = canvas.offsetWidth;
	let canvasHeight = canvas.offsetHeight;
	canvas.style.width = canvasWidth + 'px';
	canvas.width = canvasWidth * (devicePixelRatio || 1);
	canvas.height = canvasHeight * (devicePixelRatio || 1);
	if (devicePixelRatio) c.scale(devicePixelRatio, devicePixelRatio);
	c.font = document.body.style.font;

	let lastRenderFrom = 0;
	let lastRenderTo = 0;
	let lastBaseRenderFrom = 0;
	let lastBaseRenderTo = 0;
	let renderFunc = renderSelfDominators;
	let renderFuncDiff = renderTracesDiff;

	for (let mode of ['flame-mode-flame', 'flame-mode-reversed', 'flame-mode-methods']) {
		document.getElementById(mode).onclick = () => {
			selectMode(mode);
		};
	}

	document.getElementById('flame-diff-reverse').onclick = () => {
		if (heatDiffSample1 !== -1) {
			renderWrapperDiff(lastBaseRenderFrom, lastBaseRenderTo, lastRenderFrom, lastRenderTo);
		}
	};

	document.getElementById('search').onclick = () => {
		search(true);
	};

	document.getElementById('filter').onclick = () => {
		search(true, true);
	};

	const palette = [
		[0xb2e1b2, 20, 20, 20],
		[0x20d120, 50, 45, 50],
		[0x50cccc, 30, 30, 30],
		[0xe15a5a, 30, 40, 40],
		[0xc8c83c, 30, 30, 10],
		[0xe17d00, 30, 30, 0],
		[0xcce880, 20, 20, 20]
	];

	function getColorStable(p, i) {
		i = Math.imul(i, 0xcc9e2d51);
		i = Math.imul((i << 15) | (i >> 17), 0x1b873593) >>> 24;

		const v1 = (p[1] * i) >>> 8;
		const v2 = (p[2] * i) >>> 8;
		const v3 = (p[3] * i) >>> 8;
		return '#' + (p[0] + (v1 << 16 | v2 << 8 | v3)).toString(16);
	}

	function samples(n) {
		return n === 1 ? '1 sample' : n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' samples';
	}

	function pct(a, b) {
		return a >= b ? '100' : (100 * a / b).toFixed(2);
	}

	function findFrameIndex(frames, x) {
		let left = 0;
		let right = frames.length - 1;

		while (left <= right) {
			const mid = (left + right) >>> 1;
			const f = frames[mid];

			if (f.left > x) {
				right = mid - 1;
			} else if (f.left + f.width <= x) {
				left = mid + 1;
			} else {
				return mid;
			}
		}

		if (frames[left] && (frames[left].left - x) * px < 0.5) return left;
		if (frames[right] && (x - (frames[right].left + frames[right].width)) * px < 0.5) return right;

		return -1;
	}

	function findFramesBetween(frames, xFrom, xTo) {
		const result = [];
		let left = 0;
		let right = frames.length - 1;

		while (left <= right) {
			const mid = (left + right) >>> 1;
			const f = frames[mid];

			if (f.left > xFrom) {
				right = mid - 1;
			} else if (f.left + f.width <= xFrom) {
				left = mid + 1;
			} else {
				left = mid;
				break;
			}
		}
		for (let i = left; i < frames.length; i++) {
			const f = frames[i];
			if (f.left > xTo) {
				break;
			}
			result.push(f);
		}

		return result;
	}

	function findFrame(frames, x) {
		const index = findFrameIndex(frames, x);
		if (index === -1) {
			return null;
		}
		return frames[index];
	}

	function findNextFrame(frames, x) {
		const index = findFrameIndex(frames, x) + 1;
		if (index === 0 || index >= frames.length) {
			return null;
		}
		return frames[index];
	}

	function findPrevFame(frames, x) {
		const index = findFrameIndex(frames, x) - 1;
		if (index < 0) {
			return null;
		}
		return frames[index];
	}

	const marked = [];

	function render(newRoot, newLevel, minLevel) {
		minLevel = minLevel || 0;
		if (root && minLevel === 0) {
			c.fillStyle = '#ffffff';
			c.fillRect(0, 0, canvasWidth, canvasHeight);
		}

		root = newRoot || levels[0][0];
		rootLevel = newLevel || 0;
		px = canvasWidth / root.width;

		const x0 = root.left;
		const x1 = x0 + root.width;

		if (minLevel === 0) {
			marked.length = 0;
		}

		function mark(f) {
			return marked[f.left] >= f.width || (marked[f.left] = f.width);
		}

		function totalMarked() {
			let total = 0;
			let left = 0;
			Object.keys(marked).sort(function (a, b) {
				return a - b;
			}).forEach(function (x) {
				if (+x >= left) {
					total += marked[x];
					left = +x + marked[x];
				}
			});
			return total;
		}

		function drawFrame(f, y, alpha) {
			if (f.left < x1 && f.left + f.width > x0) {
				let match = showFound && pattern(f.method) && mark(f);
				const fw = f.width * px;
				if (fw < 0.1) {
					return;
				}
				c.fillStyle = match ? '#40b2ff' : f.color;
				c.fillRect((f.left - x0) * px, y, fw, 15);
				if (fw >= 21) {
					const chars = Math.floor(fw / 7);
					const title = f.title.length <= chars ? f.title : f.title.substring(0, chars - 2) + '..';
					c.fillStyle = '#000000';
					c.fillText(title, Math.max(f.left - x0, 0) * px + 3, y + 12, fw - 6);
				}

				if (alpha) {
					c.fillStyle = 'rgba(255, 255, 255, 0.25)';
					c.fillRect((f.left - x0) * px, y, fw, 15);
				}
			}
		}

		for (let h = minLevel; h < levels.length; h++) {
			const y = h * 16;
			const frames = levels[h];
			for (let f of frames) {
				drawFrame(f, y, h < rootLevel);
			}
		}

		return totalMarked();
	}

	let previewX = -1;
	let previewY = -1;

	function showPreview() {

		const w = 500;
		const h = 15 * 16;
		const s = 50;
		const dx = w / s / 2;

		preview_wrapper.style.left = (canvas.offsetLeft + Math.max(0, previewX - w - 8) - 4) + 'px';
		preview_wrapper.style.top = (previewY + canvas.offsetTop - h - 16) + 'px';
		preview_wrapper.style.display = 'block';

		let xFrom = (previewX - dx) / px + root.left;
		let xTo = (previewX + dx) / px + root.left;
		if (xFrom < 0) {
			xTo -= xFrom;
			xFrom = 0;
		}
		const preview = document.getElementById('preview');
		preview.style.width = w + "px"
		preview.style.height = h + "px";
		preview.width = w * (devicePixelRatio || 1);
		preview.height = h * (devicePixelRatio || 1);
		const c = preview.getContext('2d');
		if (devicePixelRatio) c.scale(devicePixelRatio, devicePixelRatio);
		c.font = document.body.style.font;
		c.fillStyle = '#ffffff';
		c.fillRect(0, 0, w, h);
		for (let i = 0; i < 15; i++) {
			const level = Math.floor(previewY / 16) + i - 7;
			if (level < 0 || level >= levels.length) {
				continue;
			}
			const frames = findFramesBetween(levels[level], xFrom, xTo);
			const x0 = xFrom;
			for (let f of frames) {
				const fw = f.width * s * px;
				c.fillStyle = f.color;
				c.fillRect((f.left - x0) * s * px, i * 16, fw, 15);
				if (fw >= 21) {
					const chars = Math.floor(fw / 7);
					const title = f.title.length <= chars ? f.title : f.title.substring(0, chars - 2) + '..';
					c.fillStyle = '#000000';
					c.fillText(title, (f.left - x0) * s * px + 3, i * 16 + 12, fw - 6);
				}
			}
		}
		c.strokeRect(0, 7 * 16, w, 16);
	}

	canvas.onmousemove = function (e) {
		previewX = e.offsetX;
		previewY = e.offsetY;
		if (e.shiftKey) {
			showPreview();
		} else {
			preview_wrapper.style.display = 'none';
		}

		const h = Math.floor(e.offsetY / 16);
		if (h >= 0 && h < levels.length) {
			const f = findFrame(levels[h], e.offsetX / px + root.left);
			if (f) {
				hl.style.left = (Math.max(f.left - root.left, 0) * px + canvas.offsetLeft) + 'px';
				hl.style.width = (Math.min(f.width, root.width) * px) + 'px';
				hl.style.top = (h * 16 + canvas.offsetTop) + 'px';
				hl.firstChild.textContent = f.title;
				hl.style.display = 'block';
				let details = f.details || '';
				canvas.title = f.title + '\n(' + samples(f.width) + details + ', ' + pct(f.width, levels[0][0].width) + '%)';
				canvas.style.cursor = 'pointer';
				canvas.onclick = function (e) {
					if (e.shiftKey) {
						e.preventDefault();
						searchForMethod(f.method);
						canvas.onmousemove(e);
						return;
					}

					if (e.ctrlKey) {
						const n = e.shiftKey ? findPrevFame(levels[h], e.offsetX / px + root.left) : findNextFrame(levels[h], e.offsetX / px + root.left);
						if (n) {
							render(n, h);
						}
						canvas.onmousemove(e);
						return;
					}

					if (f !== root) {
						render(f, h);
						canvas.onmousemove(e);
					}
				};
				status.textContent = 'Function: ' + canvas.title;
				status.style.display = 'inline';
				return;
			}
		}
		canvas.onmouseout();
	}

	canvas.onmouseout = function (e) {
		hl.style.display = 'none';
		status.textContent = '\xa0';
		status.style.display = 'none';
		canvas.title = '';
		canvas.style.cursor = '';
		canvas.onclick = '';
		if (e !== undefined) {
			preview_wrapper.style.display = 'none';
			previewX = previewY = -1;
		}
	}

	document.getElementById('reset').onclick = function () {
		search(false);
	}

	class DataBuffer {
		data;
		pos = 1;

		constructor(encodedData) {
			this.data = encodedData;
		}

		nextVarInt() {
			let res = 0;
			let shift = 1;
			let b;
			do {
				b = this.byteAt(this.pos++);
				res += (b % 61) * shift;
				shift *= 61;
			} while (b >= 61);
			return res;
		}

		nextBase123() {
			return (Math.imul(this.byteAt(this.pos++), 1860867)	// 123^3
				+ Math.imul(this.byteAt(this.pos++), 15129)     // 123^2
				+ Math.imul(this.byteAt(this.pos++), 123)       // 123^1
				+ this.byteAt(this.pos++)) | 0;                 // 123^0
		}

		byteAt(pos) {
			const c = this.data.charCodeAt(pos);
			switch (c) {
				case 127:
					return 0;
				case 126:
					return 13;
				case 125:
					return 38;
				case 124:
					return 60;
				case 123:
					return 62;
			}
			return c;
		}

		nextInt6() {
			return this.byteAt(this.pos++);
		}

		nextInt18() {
			return this.nextInt6() | this.nextInt6() << 6 | this.nextInt6() << 12;
		}

		int30(pos) {
			let p = pos * 5;
			if (p < 0) {
				p += this.data.length - 1;
			}
			return this.byteAt(p++) << 0
				| this.byteAt(p++) << 6
				| this.byteAt(p++) << 12
				| this.byteAt(p++) << 18
				| this.byteAt(p++) << 24;
		}
	}

	function decodeExecutions(data) {
		const samplesCount = data.int30(-1);
		const chunksCount = data.int30(-2);
		const storageSize = data.int30(-3);
		const blocksCount = data.int30(-4);
		const lz78RecordsCount = data.int30(-5);

		const starts = new Set();
		const startMethodsCount = data.nextVarInt();
		for (let i = 0; i < startMethodsCount; i++) {
			starts.add(data.nextVarInt());
		}

		// NOTE: All dynamic allocations should be done before this line
		const buffer = new ArrayBuffer((4 + 4 + 4 + 4 + 4) * lz78RecordsCount);
		const lz78RootMethods = new Uint32Array(buffer, 0, lz78RecordsCount);
		const lz78CurrentMethods = new Uint32Array(buffer, lz78RecordsCount * 4, lz78RecordsCount);
		const lz78Parents = new Uint32Array(buffer, lz78RecordsCount * 8, lz78RecordsCount);
		const lz78Sizes = new Uint32Array(buffer, lz78RecordsCount * 12, lz78RecordsCount);
		const lz78Refs = new Uint32Array(buffer, lz78RecordsCount * 16, lz78RecordsCount);

		const storageMarksIntsCount = (storageSize >> 5) + 1;
		const sampleMarksIntsCount = (samplesCount >> 5) + 1;
		const returnBuffer = new ArrayBuffer(4 * samplesCount + 4 * chunksCount + 8 * storageSize + 8 * blocksCount + 16 * zoomToGroupSize.length + 4 * storageMarksIntsCount + 4 * sampleMarksIntsCount);
		const sampleToChunkEnds = new Uint32Array(returnBuffer, 0, samplesCount);
		const chunksToStorage = new Uint32Array(returnBuffer, 4 * samplesCount, chunksCount);
		const storage = new Uint32Array(returnBuffer, 4 * samplesCount + 4 * chunksCount, storageSize);
		const storageSizes = new Uint32Array(returnBuffer, 4 * samplesCount + 4 * chunksCount + 4 * storageSize, storageSize);
		const counts = new Int32Array(returnBuffer, 4 * samplesCount + 4 * chunksCount + 8 * storageSize, blocksCount);
		const foundCounts = new Int32Array(returnBuffer, 4 * samplesCount + 4 * chunksCount + 8 * storageSize + 4 * blocksCount, blocksCount);
		const maxCounts = new Uint32Array(returnBuffer, 4 * samplesCount + 4 * chunksCount + 8 * storageSize + 8 * blocksCount, zoomToGroupSize.length);
		const searchMaxCounts = new Uint32Array(returnBuffer, 4 * samplesCount + 4 * chunksCount + 8 * storageSize + 8 * blocksCount + 4 * zoomToGroupSize.length, zoomToGroupSize.length);
		const minCounts = new Uint32Array(returnBuffer, 4 * samplesCount + 4 * chunksCount + 8 * storageSize + 8 * blocksCount + 8 * zoomToGroupSize.length, zoomToGroupSize.length);
		const searchMinCounts = new Uint32Array(returnBuffer, 4 * samplesCount + 4 * chunksCount + 8 * storageSize + 8 * blocksCount + 12 * zoomToGroupSize.length, zoomToGroupSize.length);
		const marks = new Uint32Array(returnBuffer, 4 * samplesCount + 4 * chunksCount + 8 * storageSize + 8 * blocksCount + 16 * zoomToGroupSize.length, storageMarksIntsCount);
		const sampleMarks = new Uint32Array(returnBuffer, 4 * samplesCount + 4 * chunksCount + 8 * storageSize + 8 * blocksCount + 16 * zoomToGroupSize.length + 4 * storageMarksIntsCount, sampleMarksIntsCount);

		const huffmanTableSize = data.nextVarInt();
		const huffmanMaxBits = data.nextVarInt();

		const huffmanValues = new Uint32Array(huffmanTableSize);
		const huffmanCounts = new Uint32Array(huffmanMaxBits + 1);
		for (let i = 0; i < huffmanTableSize; i++) {
			huffmanValues[i] = data.nextVarInt();
			huffmanCounts[data.nextVarInt()]++;
		}

		if (huffmanMaxBits === 0) {
			counts.fill(huffmanValues[0]);
		} else {
			let firstHuffmanCodeForCount = 0;
			let currentHuffmanCode = 0;
			let currentCountPos = 1;
			let valuesPos = 0;

			let block = 0;
			a:while (true) {
				let bits = data.nextBase123();

				for (let i = 0; i < 27; i++) {      // 27 bits per symbol
					currentHuffmanCode |= bits & 1;
					bits >>>= 1;
					let count = huffmanCounts[currentCountPos];

					let nextHuffmanCodeForCount = firstHuffmanCodeForCount + count;
					if (nextHuffmanCodeForCount > currentHuffmanCode) {
						counts[block] = huffmanValues[valuesPos + currentHuffmanCode - firstHuffmanCodeForCount];
						if (++block >= blocksCount) {
							break a;
						}
						firstHuffmanCodeForCount = 0;
						currentHuffmanCode = 0;
						currentCountPos = 1;
						valuesPos = 0;
					} else {
						firstHuffmanCodeForCount = nextHuffmanCodeForCount << 1;
						currentHuffmanCode <<= 1;
						currentCountPos++;
						valuesPos += count;
					}
				}
			}
		}

		let synonymsCount = data.nextVarInt();
		const synonyms = new Uint32Array(synonymsCount);
		for (let i = 0; i < synonymsCount; i++) {
			synonyms[i] = data.nextVarInt();
		}

		let totalCount = 0;
		for (let c = 0; c < counts.length; c++) {
			totalCount += counts[c];
			counts[c] = totalCount;
		}

		calculateMinMax(counts, minCounts, maxCounts);

		for (let recordId = 1; recordId < lz78RecordsCount; recordId++) {
			const prevChunkId = data.nextVarInt();
			const addMethodId = data.nextVarInt();

			const lzPrevRecordId = (prevChunkId < synonymsCount ? synonyms[prevChunkId] : prevChunkId) - synonymsCount;

			// 0 bit for starts, 1 bit for leafs
			const prevFlags = lz78RootMethods[lzPrevRecordId];
			lz78RootMethods[lzPrevRecordId] = prevFlags & 1;   // not a leaf anymore
			lz78RootMethods[recordId] = lzPrevRecordId === 0
				? (starts.has(addMethodId) ? 3 : 2)
				: (prevFlags | 2);  // leaf for now

			lz78CurrentMethods[recordId] = addMethodId;
			lz78Parents[recordId] = lzPrevRecordId;
			lz78Sizes[recordId] = lz78Sizes[lzPrevRecordId] + 1;
		}

		let pos = 0;
		for (let rid = 1; rid < lz78RecordsCount; rid++) {
			const flags = lz78RootMethods[rid];
			if (flags & 2) {    // leaf
				const storagePos = lz78Refs[rid];
				if (storagePos === 0) {
					let recordId = rid;
					let size = lz78Sizes[recordId];
					do {
						storageSizes[pos] = size--;
						storage[pos] = lz78CurrentMethods[recordId];
						pos++;
						lz78Refs[recordId] = pos;
						recordId = lz78Parents[recordId];
					} while (recordId !== 0 && lz78Refs[recordId] === 0);

					while (recordId !== 0) {
						storageSizes[pos] = size--;
						storage[pos] = lz78CurrentMethods[recordId];
						pos++;
						recordId = lz78Parents[recordId];
					}
				}
			}
		}

		synonymsCount = data.nextVarInt();
		for (let i = 0; i < synonymsCount; i++) {
			synonyms[i] = data.nextVarInt();
		}

		let sample = 0;
		for (let chunk = 0; chunk < chunksCount; chunk++) {
			const chunkId = data.nextVarInt();
			let recordId = (chunkId < synonymsCount ? synonyms[chunkId] : chunkId) - synonymsCount;
			const flags = lz78RootMethods[recordId];
			chunksToStorage[chunk] = recordId;
			if (flags & 1) {    // start of the next sample
				if (sample !== 0) {
					sampleToChunkEnds[sample - 1] = chunk;
				}
				sample++;
			}
		}
		if (pos > storage.length) {
			throw "storage is too low";
		}
		sampleToChunkEnds[sample - 1] = chunksCount;

		for (let chunk = 0; chunk < chunksCount; chunk++) {
			const recordId = chunksToStorage[chunk];
			if (lz78Refs[recordId] === 0) {
				throw recordId + " has no storagePos";
			}
			chunksToStorage[chunk] = lz78Refs[recordId] - 1;
		}

		return {
			sampleToChunkEnds: sampleToChunkEnds,
			chunksToStorage: chunksToStorage,
			storage: storage,
			storageSizes: storageSizes,
			counts: counts,
			foundCounts: foundCounts,
			maxCounts: maxCounts,
			searchMaxCounts: searchMaxCounts,
			minCounts: minCounts,
			searchMinCounts: searchMinCounts,
			marks: marks,
			sampleMarks: sampleMarks,
		};
	}

	class HeatmapCollection extends DataBuffer {

		minimalTimeSquare = 20; // hardcoded for now
		squaresBetweenMarks = 20; // hardcoded forever)

		zoom;
		timeSquareScale;
		timeSquareRowsCount;

		blocksData;

		constructor(encodedData) {
			super(encodedData);

			this.blocksData = decodeExecutions(this);
			this.data = undefined;
		}

		currentTimeSquareMs() {
			return this.minimalTimeSquare * this.timeSquareScale;
		}

		startBlock() {
			const timeSquareMs = this.currentTimeSquareMs();
			const timeBlockMs = timeSquareMs * this.timeSquareRowsCount * this.squaresBetweenMarks;
			return Math.floor(startMs % timeBlockMs / timeSquareMs);
		}

		count() {
			const timeSquaresCountInMarkBlock = this.timeSquareRowsCount * this.squaresBetweenMarks;
			const scaledTimeSquaresCount = Math.ceil(this.blocksData.counts.length / this.timeSquareScale) + this.startBlock();
			return Math.ceil(scaledTimeSquaresCount / timeSquaresCountInMarkBlock) * timeSquaresCountInMarkBlock;
		}

		setZoom(z) {
			this.zoom = z;
			this.timeSquareScale = zoomToGroupSize[z];
			this.timeSquareRowsCount = zoomToHeight[z];
		}

		max(search) {
			return search ? this.blocksData.searchMaxCounts[this.zoom] : this.blocksData.maxCounts[this.zoom];
		}

		min(search) {
			return search ? this.blocksData.searchMinCounts[this.zoom] : this.blocksData.minCounts[this.zoom];
		}
	}

	function claimHtml(id) {
		let e = document.getElementById(id);
		let r = e.innerHTML;
		e.remove();
		return r;
	}

	function methodName(className, methodName, bci, line, type) {
		const locationSuffix = line === 0 ? (bci === 0 ? "" : ("@" + bci)) : (":" + line);

		if (methodName === '') {
			return className + locationSuffix;
		}
		if (className === '') {
			return methodName + locationSuffix;
		}
		if (type >= 3 && type <= 5) {
			return methodName + locationSuffix;
		}

		return className + "." + methodName + locationSuffix;
	}

	function readMethods(data) {
		data.nextInt6();
		const count = data.nextVarInt();
		const names = [];
		const colors = [];
		const bcis = new Array(count + 1);

		names.push(methodName("all", "", 0, 0, 3));
		colors.push(getColorStable(palette[3], 0));

		for (let i = 0; i < count; i++) {
			const className = data.nextVarInt();
			const name = data.nextVarInt();
			const bci = data.nextInt18();
			const line = data.nextInt18();
			const type = data.nextInt6();

			const noline = bci === 0xffff && line === 0xffff;

			names.push(methodName(cpool[className], cpool[name], noline ? 0 : bci, noline ? 0 : line, type));
			colors.push(getColorStable(palette[type], i + 1));
			bcis[i + 1] = noline ? "" : ", bci: " + bci;
		}

		return {n: names, c: colors, b: bcis};
	}

	let sq = 5;
	let sqPx = sq;
	let sqScale = 1;
	const canvasScrollPadding = 200;
	const taskMaxTimeMs = 8;
	let canvasTimeHeight = 12;
	let canvasTimeHeightPx = 12;
	let startMs = /*startMs:*/0;

	const zoomToGroupSize = [
		1,              // 20 ms
		50,             // 1 s
		50 * 5,         // 5 s
	];
	const zoomToHeight = [
		50,
		60,
		60,
	];
	const zoomToText = [
		' 1 sec : 20 ms ',
		' 1 min : 1 sec ',
		' 5 min : 5 sec ',
		' 1 hr  : 1 min '
	];

	const currentHeatmap = new HeatmapCollection(claimHtml("executionsHeatmap"));
	let cpool = [/*cpool:*/];
	const methods = readMethods(new DataBuffer(claimHtml("methods")));
	cpool = null;

	let heatLastSample = -1;
	let heatActiveSample1 = -1;
	let heatActiveSample2 = -1;
	let heatDiffSample1 = -1;
	let heatDiffSample2 = -1;
	let highlightStart = -1;
	let highlightEnd = -1;

	const titleText = document.getElementById("title").textContent;
	document.getElementById("title").style.display = 'none';
	document.title = titleText;

	const heatCanvas = document.getElementById('heatmap-canvas');
	const heatCanvasWrapper = document.getElementById('heatmap-canvas-wrapper');
	const heatCanvasContainer = document.getElementById('heatmap-canvas-container');

	let heatC;
	let prevDx = -1000000000;

	const timeOptionsShort = {
		hour: '2-digit',
		minute: '2-digit',
		second: '2-digit',
		hourCycle: 'h23',
		fractionalSecondDigits: 2
	};

	const timeOptionsTiny = {
		hour: '2-digit',
		minute: '2-digit',
		second: '2-digit',
		hourCycle: 'h23'
	};

	const timeOptionsTinyWithTZ = {
		hour: '2-digit',
		minute: '2-digit',
		second: '2-digit',
		hourCycle: 'h23',
		timeZoneName: 'short'
	};

	let currentTimezone = localStorage.getItem('heatmap-timezone') || 'Local';

	function formatTime(ms, options) {
		const date = new Date(ms);
		if (currentTimezone === 'UTC') {
			return date.toLocaleTimeString(undefined, {...options, timeZone: 'UTC'});
		} else {
			return date.toLocaleTimeString(undefined, options);
		}
	}

	function getCount(counts, i) {
		if (i < 0 || counts.length === 0) {
			return 0;
		}
		return counts[Math.min(i, counts.length - 1)];
	}

	let bgInterval = null;
	let bgTasks = [];
	let cooldownTime = 0;
	let nextTask = 0;

	function addTask(id, iteration, afterFrame) {
		if (!afterFrame) afterFrame = function () {};

		for (let t of bgTasks) {
			if (t.id === id) {
				t.f = iteration;
				t.a = afterFrame;
				return;
			}
		}

		bgTasks.push({id: id, f: iteration, a: afterFrame});
		if (bgTasks.length === 1) {
			bgInterval = setInterval(function () {
				let start = performance.now();
				if (start <= cooldownTime) {
					return;
				}
				let overflow = false;
				do {
					let task = bgTasks[nextTask];
					let runAgain;
					try {
						runAgain = task.f();
					} catch (e) {
						console.error(e);
						bgTasks.length = 0;
						clearInterval(bgInterval);
						nextTask = 0;
						return;
					}

					if (runAgain) {
						nextTask++;
					} else {
						bgTasks.splice(nextTask, 1);
						if (bgTasks.length === 0) {
							clearInterval(bgInterval);
							nextTask = 0;
							return;
						}
					}

					if (nextTask >= bgTasks.length) {
						nextTask = 0;
						overflow = true;
					}
				} while (performance.now() - start < taskMaxTimeMs);

				for (let q = 0; q < overflow ? bgTasks.length : nextTask; q++) {
					bgTasks[q].a();
				}
			})
		}
	}

	function sort(array, start, end, values, bit) {
		while (true) {
			switch (end - start) {
				case 0:
				case 1:
					return;
				case 2: {
					let a = array[start];
					let b = array[start + 1];
					if (values[a] < values[b]) {
						array[start] = b;
						array[start + 1] = a;
					}
					return;
				}
			}

			let left = start;
			let right = end;

			while (true) {
				while (left < right) {
					let v = values[array[left]];
					if ((v & bit) !== 0) {
						left++;
					} else {
						break;
					}
				}
				while (left < right) {
					let v = values[array[right - 1]];
					if ((v & bit) === 0) {
						right--;
					} else {
						break;
					}
				}

				if (left < right) {
					right--;
					let l = array[left];
					array[left] = array[right];
					array[right] = l;
					left++;
				} else {
					bit >>= 1;
					if (bit === 0) {
						return;
					}
					if (end - left > right - start) {
						if (right - start > 1) {
							sort(array, start, right, values, bit);
						}
						start = left;
					} else {
						if (end - left > 1) {
							sort(array, left, end, values, bit);
						}
						end = right;
					}
					break;
				}
			}
		}
	}

	function allocateHugeArray() {
		// Note: it is very random huge number that relies on malloc laziness
		// real pre-calculation is both non-performant and fragile
		return new Uint32Array(100 * 1024 * 1024);
	}

	function renderWrapper(from, to) {
		lastRenderFrom = from;
		lastRenderTo = to;
		renderFunc(from, to);
	}

	function renderWrapperDiff(from, to, baseFrom, baseTo) {
		lastRenderFrom = from;
		lastRenderTo = to;
		lastBaseRenderFrom = baseFrom;
		lastBaseRenderTo = baseTo;
		renderFuncDiff(from, to, baseFrom, baseTo);
	}

	function reselect() {
		if (heatDiffSample1 === -1) {
			renderFunc(lastRenderFrom, lastRenderTo);
		} else {
			renderFuncDiff(lastRenderFrom, lastRenderTo, lastBaseRenderFrom, lastBaseRenderTo);
		}
	}

	function selectMode(mode) {
		switch (mode) {
			case 'flame-mode-flame':
				renderFunc = renderTraces;
				renderFuncDiff = renderTracesDiff;
				break;
			case 'flame-mode-reversed':
				renderFunc = renderTracesReversed;
				renderFuncDiff = renderTracesReversedDiff;
				break;
			case 'flame-mode-methods':
				renderFunc = renderSelfDominators;
				renderFuncDiff = renderSelfDominatorsDiff;
				break;
		}
		for (let m of ['flame-mode-flame', 'flame-mode-reversed', 'flame-mode-methods']) {
			document.getElementById(m).classList.remove('toolbarSelected');
		}
		document.getElementById(mode).classList.add('toolbarSelected');
		reselect();
	}

	function diffColor(count, baseCount, minDiff, maxDiff) {
		if (baseCount === 0) {
			return "#ffdd33";
		}
		if (baseCount === count) {
			return '#66e0f0';
		}
		if (baseCount > count) {
			const ratio = (baseCount - count) / minDiff;
			const ratioM = (ratio * 96 + 16) | 0;
			const C = 255 - Math.abs(255 - (ratioM << 1));
			const m = (255 - ratioM) - (C >> 1);

			return 'rgb(' + (((dRdl * C) >> 8) + m) + ',' + (((dGdl * C) >> 8) + m) + ',' + (((dBdl * C) >> 8) + m) + ')';
		}

		const ratio = (count - baseCount) / maxDiff;
		const ratioM = (ratio * 96 + 16) | 0;
		const C = 255 - Math.abs(255 - (ratioM << 1));
		const m = (255 - ratioM) - (C >> 1);

		return 'rgb(' + (((dRdg * C) >> 8) + m) + ',' + (((dGdg * C) >> 8) + m) + ',' + (((dBdg * C) >> 8) + m) + ')';
	}

	function prepareRender() {
		root = undefined;
		rootLevel = 0;
		c.fillStyle = '#ffffff';
		c.fillRect(0, 0, canvasWidth, canvasHeight);
		hl.style.display = 'none';
		addTask('flame', function () {
			return false;
		});
	}

	function renderTraces(from, to) {
		prepareRender();
		addTask('flamePrepare', () => {
			let [framesBuffer, sampleToChunkPos, sampleToStorageDelta, longSamplesBuffer] = prepareFrames(from, to);
			let nextFramesBuffer = allocateHugeArray();

			levels.length = 0;

			let levelToRedraw = 0;

			const ctx = {
				frames: framesBuffer,
				nextFrames: nextFramesBuffer,
				sampleToChunkPos: sampleToChunkPos,
				sampleToStorageDelta: sampleToStorageDelta,
				longSamplesBuffer: longSamplesBuffer,
				level: 0,
				chunksToStorage: currentHeatmap.blocksData.chunksToStorage,
				storage: currentHeatmap.blocksData.storage,
				sampleToChunkEnds: currentHeatmap.blocksData.sampleToChunkEnds,
				storageSizes: currentHeatmap.blocksData.storageSizes,
				methodsBuffer: new Uint32Array(methods.c.length + 1),
				methodsBufferTheOnlyChunks: new Uint32Array(methods.c.length + 1),
				methodsBufferLongSamples: new Uint32Array(methods.c.length + 1),
				methodsBufferDirectSamples: new Uint32Array(methods.c.length + 1),

				startX: 0,
				theOnlyChunksCount: 0,
				longSamplesChunksCount: 0,
				directSamplesCount: 0,

				framesPointer: 0,
				nextFramesPointer: 0,
				nextFramesCount: 0,
				methodsMapFirstPos: 0,
			};

			addTask('flame', () => {
					ctx.level = levels.length;
					const roots = nextFrames(ctx);
					if (roots.length === 0) {
						if (levels.length === 0) {
							levels.push([{left: 0, width: 0, color: methods.c[0], title: methods.n[0], method: 0}]);
						}
						renderWithMatch(levels.length * 16, root, rootLevel);
						return false;
					}

					const tmp = ctx.nextFrames;
					ctx.nextFrames = ctx.frames;
					ctx.frames = tmp;

					levels.push(roots);

					return true;
				},
				() => {
					renderWithMatch(4096, root, rootLevel, levelToRedraw);
					levelToRedraw = levels.length;
				});
			return false;
		});
	}

	function renderTracesDiff(from, to, baseFrom, baseTo) {
		prepareRender();
		let [framesBuffer, sampleToChunkPos, sampleToStorageDelta] = prepareFramesDiff(from, to, baseFrom, baseTo);
		let nextFramesBuffer = allocateHugeArray();

		levels.length = 0;

		let levelToRedraw = 0;

		let minDiff = 1;
		let maxDiff = 1;
		const ctx = {
			frames: framesBuffer,
			nextFrames: nextFramesBuffer,
			sampleToChunkPos: sampleToChunkPos,
			sampleToStorageDelta: sampleToStorageDelta,
			level: 0,
			chunksToStorage: currentHeatmap.blocksData.chunksToStorage,
			storage: currentHeatmap.blocksData.storage,
			sampleToChunkEnds: currentHeatmap.blocksData.sampleToChunkEnds,
			storageSizes: currentHeatmap.blocksData.storageSizes,

			methodsBuffer: new Uint32Array(methods.c.length + 1),
			methodsBufferTheOnlyChunks: new Uint32Array(methods.c.length + 1),
			methodsBufferDirectSamples: new Uint32Array(methods.c.length + 1),
			baseMethodsBuffer: new Uint32Array(methods.c.length + 1),
			baseMethodsBufferTheOnlyChunks: new Uint32Array(methods.c.length + 1),
			baseMethodsBufferDirectSamples: new Uint32Array(methods.c.length + 1),

			startX: 0,
			theOnlyChunksCount: 0,
			directSamplesCount: 0,
			baseTheOnlyChunksCount: 0,
			baseDirectSamplesCount: 0,

			framesPointer: 0,
			nextFramesPointer: 0,
			nextFramesCount: 0,
			methodsMapFirstPos: 0,
		};

		addTask('flame', function () {
				ctx.level = levels.length;
				const roots = nextFramesDiff(ctx);
				if (roots.length === 0) {
					if (levels.length === 0) {
						levels.push([{left: 0, width: 0, color: methods.c[0], title: methods.n[0], method: 0}]);
					}
					renderWithMatch(levels.length * 16, root, rootLevel);
					return false;
				}

				for (let root of roots) {
					if (root.baseWidth >= root.width) {
						minDiff = Math.max(minDiff, root.baseWidth - root.width);
					} else {
						maxDiff = Math.max(maxDiff, root.width - root.baseWidth);
					}
				}

				for (let root of roots) {
					root.color = diffColor(root.width, root.baseWidth, minDiff, maxDiff);
					root.title = root.title + " (" + root.width + "/" + root.baseWidth + ")";
				}

				const tmpBuffer = ctx.nextFrames;
				ctx.nextFrames = ctx.frames;
				ctx.frames = tmpBuffer;

				levels.push(roots);
				return true;
			},
			function () {
				renderWithMatch(4096, root, rootLevel, levelToRedraw);
				levelToRedraw = levels.length;
			});
	}

	function renderTracesReversed(from, to) {
		prepareRender();
		addTask('flamePrepare', () => {
			let [framesBuffer, sampleToChunkPos, sampleToStorageDelta, longSamplesBuffer] = prepareFramesReversed(from, to);
			let nextFramesBuffer = allocateHugeArray();

			levels.length = 0;

			let levelToRedraw = 0;

			const ctx = {
				frames: framesBuffer,
				nextFrames: nextFramesBuffer,
				sampleToChunkPos: sampleToChunkPos,
				sampleToStorageDelta: sampleToStorageDelta,
				longSamplesBuffer: longSamplesBuffer,
				level: 0,
				chunksToStorage: currentHeatmap.blocksData.chunksToStorage,
				storage: currentHeatmap.blocksData.storage,
				sampleToChunkEnds: currentHeatmap.blocksData.sampleToChunkEnds,
				storageSizes: currentHeatmap.blocksData.storageSizes,
				methodsBuffer: new Uint32Array(methods.c.length + 1),
				methodsBufferTheOnlyChunks: new Uint32Array(methods.c.length + 1),
				methodsBufferDirectSamples: new Uint32Array(methods.c.length + 1),

				startX: 0,
				theOnlyChunksCount: 0,
				directSamplesCount: 0,

				framesPointer: 0,
				nextFramesPointer: 0,
				nextFramesCount: 0,
				methodsMapFirstPos: 0,
			};

			addTask('flame', () => {
					ctx.level = levels.length;
					const roots = nextFramesReversed(ctx);
					if (roots.length === 0) {
						if (levels.length === 0) {
							levels.push([{left: 0, width: 0, color: methods.c[0], title: methods.n[0], method: 0}]);
						}
						renderWithMatch(levels.length * 16, root, rootLevel);
						return false;
					}

					const tmp = ctx.nextFrames;
					ctx.nextFrames = ctx.frames;
					ctx.frames = tmp;

					levels.push(roots);

					return true;
				},
				() => {
					renderWithMatch(4096, root, rootLevel, levelToRedraw);
					levelToRedraw = levels.length;
				});
			return false;
		});
	}

	function renderTracesReversedDiff(from, to, baseFrom, baseTo) {
		prepareRender();
		let [framesBuffer, sampleToChunkPos, sampleToStorageDelta] = prepareFramesReversedDiff(from, to, baseFrom, baseTo);
		let nextFramesBuffer = allocateHugeArray();

		levels.length = 0;

		let levelToRedraw = 0;

		let minDiff = 1;
		let maxDiff = 1;
		const ctx = {
			frames: framesBuffer,
			nextFrames: nextFramesBuffer,
			sampleToChunkPos: sampleToChunkPos,
			sampleToStorageDelta: sampleToStorageDelta,
			level: 0,
			chunksToStorage: currentHeatmap.blocksData.chunksToStorage,
			storage: currentHeatmap.blocksData.storage,
			sampleToChunkEnds: currentHeatmap.blocksData.sampleToChunkEnds,
			storageSizes: currentHeatmap.blocksData.storageSizes,

			methodsBuffer: new Uint32Array(methods.c.length + 1),
			methodsBufferTheOnlyChunks: new Uint32Array(methods.c.length + 1),
			methodsBufferDirectSamples: new Uint32Array(methods.c.length + 1),
			baseMethodsBuffer: new Uint32Array(methods.c.length + 1),
			baseMethodsBufferTheOnlyChunks: new Uint32Array(methods.c.length + 1),
			baseMethodsBufferDirectSamples: new Uint32Array(methods.c.length + 1),

			startX: 0,
			theOnlyChunksCount: 0,
			directSamplesCount: 0,
			baseTheOnlyChunksCount: 0,
			baseDirectSamplesCount: 0,

			framesPointer: 0,
			nextFramesPointer: 0,
			nextFramesCount: 0,
			methodsMapFirstPos: 0,
		};

		addTask('flame', function () {
				ctx.level = levels.length;
				const roots = nextFramesReversedDiff(ctx);
				if (roots.length === 0) {
					if (levels.length === 0) {
						levels.push([{left: 0, width: 0, color: methods.c[0], title: methods.n[0], method: 0}]);
					}
					renderWithMatch(levels.length * 16, root, rootLevel);
					return false;
				}

				for (let root of roots) {
					if (root.baseWidth >= root.width) {
						minDiff = Math.max(minDiff, root.baseWidth - root.width);
					} else {
						maxDiff = Math.max(maxDiff, root.width - root.baseWidth);
					}
				}

				for (let root of roots) {
					root.color = diffColor(root.width, root.baseWidth, minDiff, maxDiff);
					root.title = root.title + " (" + root.width + "/" + root.baseWidth + ")";
				}

				const tmpBuffer = ctx.nextFrames;
				ctx.nextFrames = ctx.frames;
				ctx.frames = tmpBuffer;

				levels.push(roots);
				return true;
			},
			function () {
				renderWithMatch(4096, root, rootLevel, levelToRedraw);
				levelToRedraw = levels.length;
			});
	}

	function renderSelfDominators(from, to) {
		prepareRender();
		addTask('flamePrepare', () => {
			const chunksToStorage = currentHeatmap.blocksData.chunksToStorage;
			const sampleToChunkEnds = currentHeatmap.blocksData.sampleToChunkEnds;
			const storage = currentHeatmap.blocksData.storage;
			const counts = currentHeatmap.blocksData.counts;

			const countBeforeFirstBlock = getCount(counts, from - 1);
			const countAtLastBlock = getCount(counts, to);

			const methodsCount = new Uint32Array(methods.c.length);
			const methodsBuffer = new Uint32Array(methods.c.length);
			let methodsInBuffer = 0;
			let maxCount = 0;

			for (let sampleIndex = countBeforeFirstBlock; sampleIndex < countAtLastBlock; sampleIndex++) {
				const chunkEndPos = sampleToChunkEnds[sampleIndex];
				const storagePos = chunksToStorage[chunkEndPos - 1];
				const method = storage[storagePos];
				const oldCount = methodsCount[method]++;
				if (isNaN(oldCount)) {
					throw "old count is nan";
				}
				if (oldCount === 0) {
					methodsBuffer[methodsInBuffer++] = method;
				}
				maxCount = Math.max(maxCount, oldCount);
			}

			sort(methodsBuffer, 0, methodsInBuffer, methodsCount, 1 << (31 - Math.clz32(maxCount + 1)));
			levels.length = 0;
			for (let i = 0; i < methodsInBuffer; i++) {
				const method = methodsBuffer[i];
				levels.push([{
					left: 0,
					width: methodsCount[method],
					color: methods.c[method],
					title: methods.n[method],
					method: method
				}]);
			}
			renderWithMatch(levels.length * 16, root, rootLevel);
			return false;
		});
	}

	function renderSelfDominatorsDiff(from, to, baseFrom, baseTo) {
		prepareRender();
		addTask('flamePrepare', () => {
			const chunksToStorage = currentHeatmap.blocksData.chunksToStorage;
			const sampleToChunkEnds = currentHeatmap.blocksData.sampleToChunkEnds;
			const storage = currentHeatmap.blocksData.storage;
			const counts = currentHeatmap.blocksData.counts;

			const countBeforeFirstBlock = getCount(counts, from - 1);
			const countAtLastBlock = getCount(counts, to);

			const baseCountBeforeFirstBlock = getCount(counts, baseFrom - 1);
			const baseCountAtLastBlock = getCount(counts, baseTo);

			const methodsCount = new Uint32Array(methods.c.length);
			const baseMethodsCount = new Uint32Array(methods.c.length);
			const methodsBuffer = new Uint32Array(methods.c.length);
			let methodsInBuffer = 0;
			let maxCount = 1;
			let minDiff = 1;
			let maxDiff = 1;

			for (let sampleIndex = countBeforeFirstBlock; sampleIndex < countAtLastBlock; sampleIndex++) {
				const chunkEndPos = sampleToChunkEnds[sampleIndex];
				const storagePos = chunksToStorage[chunkEndPos - 1];
				const method = storage[storagePos];
				const oldCount = methodsCount[method]++;
				if (oldCount === 0) {
					methodsBuffer[methodsInBuffer++] = method;
				}
				maxCount = Math.max(maxCount, oldCount);
			}

			for (let sampleIndex = baseCountBeforeFirstBlock; sampleIndex < baseCountAtLastBlock; sampleIndex++) {
				const chunkEndPos = sampleToChunkEnds[sampleIndex];
				const storagePos = chunksToStorage[chunkEndPos - 1];
				const method = storage[storagePos];
				const oldCount = baseMethodsCount[method]++;
				if (oldCount === 0 && methodsCount[method] === 0) {
					methodsBuffer[methodsInBuffer++] = method;
				}
			}

			for (let i = 0; i < methodsInBuffer; i++) {
				const method = methodsBuffer[i];
				const count = methodsCount[method];
				const baseCount = baseMethodsCount[method];

				if (baseCount > count) {
					minDiff = Math.max(minDiff, baseCount - count);
				} else {
					maxDiff = Math.max(maxDiff, count - baseCount);
				}
			}

			sort(methodsBuffer, 0, methodsInBuffer, methodsCount, 1 << (31 - Math.clz32(maxCount)));

			levels.length = 0;
			for (let i = 0; i < methodsInBuffer; i++) {
				const method = methodsBuffer[i];
				const count = methodsCount[method];
				const baseCount = baseMethodsCount[method];
				const title = methods.n[method] + " (" + count + "/" + baseCount + ")"

				let color = diffColor(count, baseCount, minDiff, maxDiff);
				levels.push([{left: 0, width: count, color: color, title: title, method: method}]);
			}
			renderWithMatch(levels.length * 16, root, rootLevel);
			return false;
		});
	}

	function prepareFrames(from, to) {
		const chunksToStorage = currentHeatmap.blocksData.chunksToStorage;
		const sampleToChunkEnds = currentHeatmap.blocksData.sampleToChunkEnds;
		const storageSizes = currentHeatmap.blocksData.storageSizes;
		const counts = currentHeatmap.blocksData.counts;

		const countBeforeFirstBlock = getCount(counts, from - 1);
		const countAtLastBlock = getCount(counts, to);

		const theOnlyChunksBuffer = allocateHugeArray();
		const firstChunkPosOfLongSamplesBuffer = allocateHugeArray();
		const sampleToChunkPos = new Uint32Array(sampleToChunkEnds.length);
		const sampleToStorageDelta = new Uint32Array(sampleToChunkEnds.length);
		const firstChunkPosOfLongSamples = new Uint32Array(storageSizes.length);
		const theOnlyChunks = new Uint32Array(storageSizes.length);

		let theOnlyChunksBufferPos = 0;

		let firstChunkPosOfLongSamplesPos = 0;
		let totalCountLongSamples = 0;
		let count = 0;
		for (let sampleIndex = countBeforeFirstBlock; sampleIndex < countAtLastBlock; sampleIndex++) {
			if (filterOut(sampleIndex)) {
				continue;
			}
			count++;
			const chunkPos = sampleToChunkEnds[sampleIndex - 1] || 0;
			const chunkEndPos = sampleToChunkEnds[sampleIndex];
			const storagePos = chunksToStorage[chunkPos];
			if (chunkPos + 1 === chunkEndPos) {
				if (theOnlyChunks[storagePos]++ === 0) {
					theOnlyChunksBuffer[theOnlyChunksBufferPos++] = storagePos;
				}
			} else {
				if (firstChunkPosOfLongSamples[storagePos]++ === 0) {
					firstChunkPosOfLongSamplesBuffer[firstChunkPosOfLongSamplesPos++] = storagePos;
				}
				totalCountLongSamples++;
			}
		}

		const framesBuffer = allocateHugeArray();
		framesBuffer[0] = count === 0 ? 0 : 1;    // frames count on this level

		framesBuffer[1] = 0;    // methodId
		framesBuffer[2] = 0;    // x
		framesBuffer[3] = count;
		framesBuffer[4] = theOnlyChunksBufferPos;
		framesBuffer[5] = firstChunkPosOfLongSamplesPos;
		framesBuffer[6] = 0;    // direct samples count

		let framesBufferPos = 7;

		for (let storagePosIndex = 0; storagePosIndex < theOnlyChunksBufferPos; storagePosIndex++) {
			const storagePos = theOnlyChunksBuffer[storagePosIndex];
			framesBuffer[framesBufferPos++] = storagePos;
			framesBuffer[framesBufferPos++] = theOnlyChunks[storagePos];
		}

		const longSamplesBuffer = new Uint32Array(totalCountLongSamples);
		let longSamplesBufferPos = 0;
		for (let storagePosIndex = 0; storagePosIndex < firstChunkPosOfLongSamplesPos; storagePosIndex++) {
			const storagePos = firstChunkPosOfLongSamplesBuffer[storagePosIndex];
			const count = firstChunkPosOfLongSamples[storagePos];
			framesBuffer[framesBufferPos++] = storagePos;
			framesBuffer[framesBufferPos++] = count;
			framesBuffer[framesBufferPos++] = longSamplesBufferPos;

			firstChunkPosOfLongSamples[storagePos] = longSamplesBufferPos;
			longSamplesBufferPos += count;
		}

		for (let sampleIndex = countBeforeFirstBlock; sampleIndex < countAtLastBlock; sampleIndex++) {
			if (filterOut(sampleIndex)) {
				continue;
			}
			const chunkPos = sampleToChunkEnds[sampleIndex - 1] || 0;
			const chunkEndPos = sampleToChunkEnds[sampleIndex];
			const storagePos = chunksToStorage[chunkPos];
			if (chunkPos + 1 !== chunkEndPos) {
				longSamplesBuffer[firstChunkPosOfLongSamples[storagePos]++] = sampleIndex;
			}
		}

		return [framesBuffer, sampleToChunkPos, sampleToStorageDelta, longSamplesBuffer];
	}

	function prepareFramesDiff(from, to, baseFrom, baseTo) {
		const chunksToStorage = currentHeatmap.blocksData.chunksToStorage;
		const sampleToChunkEnds = currentHeatmap.blocksData.sampleToChunkEnds;
		const storageSizes = currentHeatmap.blocksData.storageSizes;
		const counts = currentHeatmap.blocksData.counts;

		const countBeforeFirstBlock = getCount(counts, from - 1);
		const countAtLastBlock = getCount(counts, to);

		const baseCountBeforeFirstBlock = getCount(counts, baseFrom - 1);
		const baseCountAtLastBlock = getCount(counts, baseTo);

		const theOnlyChunksBuffer = allocateHugeArray();
		const sampleToChunkPos = new Uint32Array(sampleToChunkEnds.length);
		const sampleToStorageDelta = new Uint32Array(sampleToChunkEnds.length);
		const theOnlyChunks = new Uint32Array(storageSizes.length);
		const baseTheOnlyChunks = new Uint32Array(storageSizes.length);

		let theOnlyChunksBufferPos = 0;
		let singleSamplesCount = 0;
		let count = 0;
		for (let sampleIndex = countBeforeFirstBlock; sampleIndex < countAtLastBlock; sampleIndex++) {
			if (filterOut(sampleIndex)) {
				continue;
			}
			count++;
			const chunkPos = sampleToChunkEnds[sampleIndex - 1] || 0;
			const chunkEndPos = sampleToChunkEnds[sampleIndex];
			if (chunkPos + 1 === chunkEndPos) {
				const storagePos = chunksToStorage[chunkPos];
				if (theOnlyChunks[storagePos]++ === 0) {
					theOnlyChunksBuffer[theOnlyChunksBufferPos++] = storagePos;
				}
			} else {
				singleSamplesCount++;
			}
		}

		const theOnlyChunksCount = theOnlyChunksBufferPos;
		let baseSingleSamplesCount = 0;
		let baseCount = 0;
		for (let sampleIndex = baseCountBeforeFirstBlock; sampleIndex < baseCountAtLastBlock; sampleIndex++) {
			if (filterOut(sampleIndex)) {
				continue;
			}
			baseCount++;
			const chunkPos = sampleToChunkEnds[sampleIndex - 1] || 0;
			const chunkEndPos = sampleToChunkEnds[sampleIndex];
			if (chunkPos + 1 === chunkEndPos) {
				const storagePos = chunksToStorage[chunkPos];
				if (baseTheOnlyChunks[storagePos]++ === 0) {
					theOnlyChunksBuffer[theOnlyChunksBufferPos++] = storagePos;
				}
			} else {
				baseSingleSamplesCount++;
			}
		}

		const framesBuffer = allocateHugeArray();
		framesBuffer[0] = 1;    // frames count on this level

		framesBuffer[1] = 0;    // methodId
		framesBuffer[2] = 0;    // x
		framesBuffer[3] = count;
		framesBuffer[4] = theOnlyChunksCount;
		framesBuffer[5] = singleSamplesCount;
		framesBuffer[6] = baseCount;
		framesBuffer[7] = theOnlyChunksBufferPos - theOnlyChunksCount;
		framesBuffer[8] = baseSingleSamplesCount;

		let framesBufferPos = 9;

		for (let storagePosIndex = 0; storagePosIndex < theOnlyChunksCount; storagePosIndex++) {
			const storagePos = theOnlyChunksBuffer[storagePosIndex];
			framesBuffer[framesBufferPos++] = storagePos;
			framesBuffer[framesBufferPos++] = theOnlyChunks[storagePos];
		}

		for (let sampleIndex = countBeforeFirstBlock; sampleIndex < countAtLastBlock; sampleIndex++) {
			if (filterOut(sampleIndex)) {
				continue;
			}
			const chunkPos = sampleToChunkEnds[sampleIndex - 1] || 0;
			const chunkEndPos = sampleToChunkEnds[sampleIndex];
			if (chunkPos + 1 !== chunkEndPos) {
				sampleToChunkPos[sampleIndex] = chunkPos;

				const storagePos = chunksToStorage[chunkPos];
				sampleToStorageDelta[sampleIndex] = storageSizes[storagePos] | 0x80000000;

				framesBuffer[framesBufferPos++] = sampleIndex;
			}
		}

		for (let storagePosIndex = theOnlyChunksCount; storagePosIndex < theOnlyChunksBufferPos; storagePosIndex++) {
			const storagePos = theOnlyChunksBuffer[storagePosIndex];
			framesBuffer[framesBufferPos++] = storagePos;
			framesBuffer[framesBufferPos++] = baseTheOnlyChunks[storagePos];
		}

		for (let sampleIndex = baseCountBeforeFirstBlock; sampleIndex < baseCountAtLastBlock; sampleIndex++) {
			if (filterOut(sampleIndex)) {
				continue;
			}
			const chunkPos = sampleToChunkEnds[sampleIndex - 1] || 0;
			const chunkEndPos = sampleToChunkEnds[sampleIndex];
			if (chunkPos + 1 !== chunkEndPos) {
				if (sampleToStorageDelta[sampleIndex] === 0) {
					sampleToChunkPos[sampleIndex] = chunkPos;

					const storagePos = chunksToStorage[chunkPos];
					sampleToStorageDelta[sampleIndex] = storageSizes[storagePos];
				}
				framesBuffer[framesBufferPos++] = sampleIndex;
			}
		}

		return [framesBuffer, sampleToChunkPos, sampleToStorageDelta];
	}

	function prepareFramesReversed(from, to) {
		const chunksToStorage = currentHeatmap.blocksData.chunksToStorage;
		const sampleToChunkEnds = currentHeatmap.blocksData.sampleToChunkEnds;
		const storageSizes = currentHeatmap.blocksData.storageSizes;
		const counts = currentHeatmap.blocksData.counts;

		const countBeforeFirstBlock = getCount(counts, from - 1);
		const countAtLastBlock = getCount(counts, to);

		const theOnlyChunksBuffer = allocateHugeArray();
		const sampleToChunkPos = new Uint32Array(sampleToChunkEnds.length);
		const sampleToStorageDelta = new Uint32Array(sampleToChunkEnds.length);
		const theOnlyChunks = new Uint32Array(storageSizes.length);

		let theOnlyChunksBufferPos = 0;
		let singleSamplesCount = 0;
		let count = 0;
		for (let sampleIndex = countBeforeFirstBlock; sampleIndex < countAtLastBlock; sampleIndex++) {
			if (filterOut(sampleIndex)) {
				continue;
			}
			count++;
			const chunkPos = sampleToChunkEnds[sampleIndex - 1] || 0;
			const chunkEndPos = sampleToChunkEnds[sampleIndex];
			if (chunkPos === chunkEndPos - 1) {
				const storagePos = chunksToStorage[chunkPos];
				if (theOnlyChunks[storagePos]++ === 0) {
					theOnlyChunksBuffer[theOnlyChunksBufferPos++] = storagePos;
				}
			} else {
				singleSamplesCount++;
			}
		}

		const framesBuffer = allocateHugeArray();
		framesBuffer[0] = count === 0 ? 0 : 1;    // frames count on this level

		framesBuffer[1] = 0;    // methodId
		framesBuffer[2] = 0;    // x
		framesBuffer[3] = count;
		framesBuffer[4] = theOnlyChunksBufferPos;
		framesBuffer[5] = singleSamplesCount;    // direct samples count

		let framesBufferPos = 6;

		for (let storagePosIndex = 0; storagePosIndex < theOnlyChunksBufferPos; storagePosIndex++) {
			const storagePos = theOnlyChunksBuffer[storagePosIndex];
			framesBuffer[framesBufferPos++] = storagePos;
			framesBuffer[framesBufferPos++] = theOnlyChunks[storagePos];
		}

		for (let sampleIndex = countBeforeFirstBlock; sampleIndex < countAtLastBlock; sampleIndex++) {
			if (filterOut(sampleIndex)) {
				continue;
			}
			const chunkPos = sampleToChunkEnds[sampleIndex - 1] || 0;
			const chunkEndPos = sampleToChunkEnds[sampleIndex];
			if (chunkPos !== chunkEndPos - 1) {
				sampleToChunkPos[sampleIndex] = chunkEndPos - 1;
				sampleToStorageDelta[sampleIndex] = 0;

				framesBuffer[framesBufferPos++] = sampleIndex;
			}
		}

		return [framesBuffer, sampleToChunkPos, sampleToStorageDelta];
	}

	function prepareFramesReversedDiff(from, to, baseFrom, baseTo) {
		const chunksToStorage = currentHeatmap.blocksData.chunksToStorage;
		const sampleToChunkEnds = currentHeatmap.blocksData.sampleToChunkEnds;
		const storageSizes = currentHeatmap.blocksData.storageSizes;
		const counts = currentHeatmap.blocksData.counts;

		const countBeforeFirstBlock = getCount(counts, from - 1);
		const countAtLastBlock = getCount(counts, to);

		const baseCountBeforeFirstBlock = getCount(counts, baseFrom - 1);
		const baseCountAtLastBlock = getCount(counts, baseTo);

		const theOnlyChunksBuffer = allocateHugeArray();
		const sampleToChunkPos = new Uint32Array(sampleToChunkEnds.length);
		const sampleToStorageDelta = new Uint32Array(sampleToChunkEnds.length);
		const theOnlyChunks = new Uint32Array(storageSizes.length);
		const baseTheOnlyChunks = new Uint32Array(storageSizes.length);

		let theOnlyChunksBufferPos = 0;
		let singleSamplesCount = 0;
		let count = 0;
		for (let sampleIndex = countBeforeFirstBlock; sampleIndex < countAtLastBlock; sampleIndex++) {
			if (filterOut(sampleIndex)) {
				continue;
			}
			count++;
			const chunkPos = sampleToChunkEnds[sampleIndex - 1] || 0;
			const chunkEndPos = sampleToChunkEnds[sampleIndex];
			if (chunkPos + 1 === chunkEndPos) {
				const storagePos = chunksToStorage[chunkPos];
				if (theOnlyChunks[storagePos]++ === 0) {
					theOnlyChunksBuffer[theOnlyChunksBufferPos++] = storagePos;
				}
			} else {
				singleSamplesCount++;
			}
		}

		const theOnlyChunksCount = theOnlyChunksBufferPos;
		let baseSingleSamplesCount = 0;
		let baseCount = 0;
		for (let sampleIndex = baseCountBeforeFirstBlock; sampleIndex < baseCountAtLastBlock; sampleIndex++) {
			if (filterOut(sampleIndex)) {
				continue;
			}
			baseCount++;
			const chunkPos = sampleToChunkEnds[sampleIndex - 1] || 0;
			const chunkEndPos = sampleToChunkEnds[sampleIndex];
			if (chunkPos + 1 === chunkEndPos) {
				const storagePos = chunksToStorage[chunkPos];
				if (baseTheOnlyChunks[storagePos]++ === 0) {
					theOnlyChunksBuffer[theOnlyChunksBufferPos++] = storagePos;
				}
			} else {
				baseSingleSamplesCount++;
			}
		}

		const framesBuffer = allocateHugeArray();
		framesBuffer[0] = 1;    // frames count on this level

		framesBuffer[1] = 0;    // methodId
		framesBuffer[2] = 0;    // x
		framesBuffer[3] = count;
		framesBuffer[4] = theOnlyChunksCount;
		framesBuffer[5] = singleSamplesCount;
		framesBuffer[6] = baseCount;
		framesBuffer[7] = theOnlyChunksBufferPos - theOnlyChunksCount;
		framesBuffer[8] = baseSingleSamplesCount;

		let framesBufferPos = 9;

		for (let storagePosIndex = 0; storagePosIndex < theOnlyChunksCount; storagePosIndex++) {
			const storagePos = theOnlyChunksBuffer[storagePosIndex];
			framesBuffer[framesBufferPos++] = storagePos;
			framesBuffer[framesBufferPos++] = theOnlyChunks[storagePos];
		}

		for (let sampleIndex = countBeforeFirstBlock; sampleIndex < countAtLastBlock; sampleIndex++) {
			if (filterOut(sampleIndex)) {
				continue;
			}
			const chunkPos = sampleToChunkEnds[sampleIndex - 1] || 0;
			const chunkEndPos = sampleToChunkEnds[sampleIndex];
			if (chunkPos + 1 !== chunkEndPos) {
				sampleToChunkPos[sampleIndex] = chunkEndPos - 1;
				sampleToStorageDelta[sampleIndex] = 0x80000000;

				framesBuffer[framesBufferPos++] = sampleIndex;
			}
		}

		for (let storagePosIndex = theOnlyChunksCount; storagePosIndex < theOnlyChunksBufferPos; storagePosIndex++) {
			const storagePos = theOnlyChunksBuffer[storagePosIndex];
			framesBuffer[framesBufferPos++] = storagePos;
			framesBuffer[framesBufferPos++] = baseTheOnlyChunks[storagePos];
		}

		for (let sampleIndex = baseCountBeforeFirstBlock; sampleIndex < baseCountAtLastBlock; sampleIndex++) {
			if (filterOut(sampleIndex)) {
				continue;
			}
			const chunkPos = sampleToChunkEnds[sampleIndex - 1] || 0;
			const chunkEndPos = sampleToChunkEnds[sampleIndex];
			if (chunkPos + 1 !== chunkEndPos) {
				if (sampleToStorageDelta[sampleIndex] === 0) {
					sampleToChunkPos[sampleIndex] = chunkEndPos - 1;
					sampleToStorageDelta[sampleIndex] = 0x80000000;
				}
				framesBuffer[framesBufferPos++] = sampleIndex;
			}
		}

		return [framesBuffer, sampleToChunkPos, sampleToStorageDelta];
	}

	function filterOut(sample) {
		if (!filterFrames) {
			return false;
		}
		const sampleMarks = currentHeatmap.blocksData.sampleMarks;
		return (sampleMarks[sample >> 5] & (1 << (sample & 31))) === 0;
	}

	function nextFrames(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;

		ctx.startX = 0;
		ctx.theOnlyChunksCount = 0;
		ctx.longSamplesChunksCount = 0;
		ctx.directSamplesCount = 0;

		ctx.nextFramesPointer = 1; // one reserved to place nextFramesCount
		ctx.nextFramesCount = 0;

		let fp = 0;
		const framesCount = frames[fp++];

		const roots = new Array(framesCount);
		for (let frame = 0; frame < framesCount; frame++) {
			const methodId = frames[fp++];
			const x = frames[fp++];
			const childrenCount = frames[fp++];

			ctx.startX = x;
			ctx.theOnlyChunksCount = frames[fp++];
			ctx.longSamplesChunksCount = frames[fp++];
			ctx.directSamplesCount = frames[fp++];

			roots[frame] = {
				left: x,
				width: childrenCount,
				color: methods.c[methodId],
				title: methods.n[methodId],
				details: methods.b[methodId],
				method: methodId
			};

			ctx.framesPointer = fp;
			ctx.methodsMapFirstPos = ctx.nextFrames.length;
			processFrame(ctx)
			fp = ctx.framesPointer;
		}

		nextFrames[0] = ctx.nextFramesCount;

		return roots;
	}

	function nextFramesDiff(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;

		ctx.startX = 0;
		ctx.theOnlyChunksCount = 0;
		ctx.directSamplesCount = 0;

		ctx.nextFramesPointer = 1; // one reserved to place nextFramesCount
		ctx.nextFramesCount = 0;

		let fp = 0;
		const framesCount = frames[fp++];

		const roots = new Array(framesCount);
		for (let frame = 0; frame < framesCount; frame++) {
			const methodId = frames[fp++];
			let x = frames[fp++];

			const childrenCount = frames[fp++];
			const theOnlyChunksCount = frames[fp++];
			const directSamplesCount = frames[fp++];

			const baseChildrenCount = frames[fp++];
			const baseTheOnlyChunksCount = frames[fp++];
			const baseDirectSamplesCount = frames[fp++];

			ctx.startX = x;
			ctx.theOnlyChunksCount = theOnlyChunksCount;
			ctx.directSamplesCount = directSamplesCount;
			ctx.baseChildrenCount = baseChildrenCount;
			ctx.baseTheOnlyChunksCount = baseTheOnlyChunksCount;
			ctx.baseDirectSamplesCount = baseDirectSamplesCount;

			roots[frame] = {
				left: x,
				width: childrenCount,
				baseWidth: baseChildrenCount,
				color: methods.c[methodId],
				title: methods.n[methodId],
				details: methods.b[methodId],
				method: methodId,
			};

			ctx.framesPointer = fp;
			ctx.methodsMapFirstPos = ctx.nextFrames.length;
			processFrameDiff(ctx)
			fp = ctx.framesPointer;
		}

		nextFrames[0] = ctx.nextFramesCount;

		return roots;
	}

	function nextFramesReversed(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;

		ctx.startX = 0;
		ctx.theOnlyChunksCount = 0;
		ctx.directSamplesCount = 0;

		ctx.nextFramesPointer = 1; // one reserved to place nextFramesCount
		ctx.nextFramesCount = 0;

		let fp = 0;
		const framesCount = frames[fp++];

		const roots = new Array(framesCount);
		for (let frame = 0; frame < framesCount; frame++) {
			const methodId = frames[fp++];
			const x = frames[fp++];
			const childrenCount = frames[fp++];

			ctx.startX = x;
			ctx.theOnlyChunksCount = frames[fp++];
			ctx.directSamplesCount = frames[fp++];

			roots[frame] = {
				left: x,
				width: childrenCount,
				color: methods.c[methodId],
				title: methods.n[methodId],
				details: methods.b[methodId],
				method: methodId
			};

			ctx.framesPointer = fp;
			ctx.methodsMapFirstPos = ctx.nextFrames.length;
			processFrameReversed(ctx)
			fp = ctx.framesPointer;
		}

		nextFrames[0] = ctx.nextFramesCount;

		return roots;
	}

	function nextFramesReversedDiff(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;

		ctx.startX = 0;
		ctx.theOnlyChunksCount = 0;
		ctx.directSamplesCount = 0;

		ctx.nextFramesPointer = 1; // one reserved to place nextFramesCount
		ctx.nextFramesCount = 0;

		let fp = 0;
		const framesCount = frames[fp++];

		const roots = new Array(framesCount);
		for (let frame = 0; frame < framesCount; frame++) {
			const methodId = frames[fp++];
			let x = frames[fp++];

			const childrenCount = frames[fp++];
			const theOnlyChunksCount = frames[fp++];
			const directSamplesCount = frames[fp++];

			const baseChildrenCount = frames[fp++];
			const baseTheOnlyChunksCount = frames[fp++];
			const baseDirectSamplesCount = frames[fp++];

			ctx.startX = x;
			ctx.theOnlyChunksCount = theOnlyChunksCount;
			ctx.directSamplesCount = directSamplesCount;
			ctx.baseChildrenCount = baseChildrenCount;
			ctx.baseTheOnlyChunksCount = baseTheOnlyChunksCount;
			ctx.baseDirectSamplesCount = baseDirectSamplesCount;

			roots[frame] = {
				left: x,
				width: childrenCount,
				baseWidth: baseChildrenCount,
				color: methods.c[methodId],
				title: methods.n[methodId],
				details: methods.b[methodId],
				method: methodId,
			};

			ctx.framesPointer = fp;
			ctx.methodsMapFirstPos = ctx.nextFrames.length;
			processFrameReversedDiff(ctx)
			fp = ctx.framesPointer;
		}

		nextFrames[0] = ctx.nextFramesCount;

		return roots;
	}

	function processFrame(ctx) {
		const startFramesPointer = ctx.framesPointer;

		// the main idea is to go through the samples and calculate total amount of samples for every method, using method buffers
		let totalChildren = 0;
		// calculates count of each method in methodsBufferTheOnlyChunks (for the-only-one-chunk samples)
		totalChildren += prepareTheOnlyChunks(ctx);
		// calculates count of each method in methodsBufferLongSamples (for long samples, that shares the first chunk)
		totalChildren += prepareLongSamplesChunks(ctx);
		// calculates count of each method in methodsBufferDirectSamples (for direct samples, expanded to list of methods)
		totalChildren += prepareDirectSamples(ctx);

		if (totalChildren === 0) {
			return;
		}

		// frames are sorted from more frequent to less frequent
		sortFramesForWrite(ctx);
		// writes frames data and reserves place for splitting samples between methods
		writeNextFrames(ctx);

		// the main idea is to go through the samples again and split them by methods into prepared buffer
		ctx.framesPointer = startFramesPointer;
		// splits the-only-one-chunk samples, skips finished samples
		splitTheOnlyChunks(ctx);
		// splits long samples, unpacks finished chunks to direct samples
		splitLongSamplesChunks(ctx);
		// splits direct samples, skips finished samples
		splitDirectSamples(ctx);

		// cleans buffers and moves nextFramesPointer
		cleanupMethodBuffers(ctx);

		const methodCount = ctx.nextFrames.length - ctx.methodsMapFirstPos;
		ctx.nextFramesCount += methodCount;
	}

	function processFrameDiff(ctx) {
		const startFramesPointer = ctx.framesPointer;

		// the main idea is to go through the samples and calculate total amount of samples for every method, using method buffers
		let totalChildren = 0;
		// calculates count of each method in methodsBufferTheOnlyChunks (for the-only-one-chunk samples)
		totalChildren += prepareTheOnlyChunks(ctx);

		// NOTE: there is no support for long samples from several chunks for diffs, they are split to direct samples.
		// It gives only 10% performance, so it is not critical for any practical case of using diffs.

		// calculates count of each method in methodsBufferDirectSamples (for direct samples, expanded to list of methods)
		totalChildren += prepareDirectSamplesDiff(ctx);

		const startBaseFramesPointer = ctx.framesPointer;
		// calculates count of each method in baseMethodsBufferTheOnlyChunks (for the-only-one-chunk samples of base)
		prepareBaseTheOnlyChunks(ctx);
		// calculates count of each method in baseMethodsBufferDirectSamples (for direct samples of base, expanded to list of methods)
		prepareBaseDirectSamples(ctx);

		if (totalChildren === 0) {
			return;
		}

		const endFramesPointer = ctx.framesPointer;

		// frames are sorted from more frequent to less frequent
		sortFramesForWrite(ctx);
		// writes frames data and reserves place for splitting samples between methods
		writeNextFramesDiff(ctx);

		// the main idea is to go through the samples again and split them by methods into prepared buffer
		// we need to start from base methods to handle the case when the same sample contained both in base and current
		ctx.framesPointer = startBaseFramesPointer;
		splitBaseTheOnlyChunks(ctx);
		splitBaseDirectSamples(ctx);

		// the main idea is to go through the samples again and split them by methods into prepared buffer
		ctx.framesPointer = startFramesPointer;
		// splits the-only-one-chunk samples, skips finished samples
		splitTheOnlyChunks(ctx);
		// splits direct samples, skips finished samples
		splitDirectSamplesDiff(ctx);

		// cleans buffers and moves nextFramesPointer
		cleanupMethodBuffersDiff(ctx);

		const methodCount = ctx.nextFrames.length - ctx.methodsMapFirstPos;
		ctx.nextFramesCount += methodCount;
		ctx.framesPointer = endFramesPointer;
	}

	function processFrameReversed(ctx) {
		const startFramesPointer = ctx.framesPointer;

		// the main idea is to go through the samples and calculate total amount of samples for every method, using method buffers
		let totalChildren = 0;
		// calculates count of each method in methodsBufferTheOnlyChunks (for the-only-one-chunk samples)
		totalChildren += prepareTheOnlyChunksReversed(ctx);
		// calculates count of each method in methodsBufferDirectSamples (for direct samples, expanded to list of methods)
		totalChildren += prepareDirectSamplesReversed(ctx);

		if (totalChildren === 0) {
			return;
		}

		// frames are sorted from more frequent to less frequent
		sortFramesForWrite(ctx);
		// writes frames data and reserves place for splitting samples between methods
		writeNextFramesReversed(ctx);

		// the main idea is to go through the samples again and split them by methods into prepared buffer
		ctx.framesPointer = startFramesPointer;
		// splits the-only-one-chunk samples, skips finished samples
		splitTheOnlyChunksReversed(ctx);
		// splits direct samples, skips finished samples
		splitDirectSamplesReversed(ctx);

		// cleans buffers and moves nextFramesPointer
		cleanupMethodBuffersReversed(ctx);

		const methodCount = ctx.nextFrames.length - ctx.methodsMapFirstPos;
		ctx.nextFramesCount += methodCount;
	}

	function processFrameReversedDiff(ctx) {
		const startFramesPointer = ctx.framesPointer;

		// the main idea is to go through the samples and calculate total amount of samples for every method, using method buffers
		let totalChildren = 0;
		// calculates count of each method in methodsBufferTheOnlyChunks (for the-only-one-chunk samples)
		totalChildren += prepareTheOnlyChunksReversed(ctx);

		// calculates count of each method in methodsBufferDirectSamples (for direct samples, expanded to list of methods)
		totalChildren += prepareDirectSamplesReversedDiff(ctx);

		const startBaseFramesPointer = ctx.framesPointer;
		// calculates count of each method in baseMethodsBufferTheOnlyChunks (for the-only-one-chunk samples of base)
		prepareBaseTheOnlyChunksReversed(ctx);
		// calculates count of each method in baseMethodsBufferDirectSamples (for direct samples of base, expanded to list of methods)
		prepareBaseDirectSamplesReversed(ctx);

		if (totalChildren === 0) {
			return;
		}

		const endFramesPointer = ctx.framesPointer;

		// frames are sorted from more frequent to less frequent
		sortFramesForWrite(ctx);
		// writes frames data and reserves place for splitting samples between methods
		writeNextFramesDiff(ctx);

		// the main idea is to go through the samples again and split them by methods into prepared buffer
		// we need to start from base methods to handle the case when the same sample contained both in base and current
		ctx.framesPointer = startBaseFramesPointer;
		splitBaseTheOnlyChunksReversed(ctx);
		splitBaseDirectSamplesReversed(ctx);

		// the main idea is to go through the samples again and split them by methods into prepared buffer
		ctx.framesPointer = startFramesPointer;
		// splits the-only-one-chunk samples, skips finished samples
		splitTheOnlyChunksReversed(ctx);
		// splits direct samples, skips finished samples
		splitDirectSamplesReversedDiff(ctx);

		// cleans buffers and moves nextFramesPointer
		cleanupMethodBuffersDiff(ctx);

		const methodCount = ctx.nextFrames.length - ctx.methodsMapFirstPos;
		ctx.nextFramesCount += methodCount;
		ctx.framesPointer = endFramesPointer;
	}

	function prepareTheOnlyChunks(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const level = ctx.level;
		const storage = ctx.storage;
		const storageSizes = ctx.storageSizes;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferTheOnlyChunks = ctx.methodsBufferTheOnlyChunks;

		const theOnlyChunksCount = ctx.theOnlyChunksCount;

		const levelPlusOne = level + 1;

		let fp = ctx.framesPointer;
		let methodsMapFirstPos = ctx.methodsMapFirstPos;

		let children = 0;
		for (let child = 0; child < theOnlyChunksCount; child++) {
			const storagePos = frames[fp++];
			const count = frames[fp++];
			const size = storageSizes[storagePos];
			if (size === level) {
				continue;
			}
			const methodId = storage[storagePos + size - levelPlusOne];

			if ((methodsBuffer[methodId] += count) === count) {
				nextFrames[--methodsMapFirstPos] = methodId;
			}
			methodsBufferTheOnlyChunks[methodId]++;

			children++;
		}

		ctx.framesPointer = fp;
		ctx.methodsMapFirstPos = methodsMapFirstPos;

		return children;
	}

	function prepareTheOnlyChunksReversed(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const level = ctx.level;
		const storage = ctx.storage;
		const storageSizes = ctx.storageSizes;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferTheOnlyChunks = ctx.methodsBufferTheOnlyChunks;

		const theOnlyChunksCount = ctx.theOnlyChunksCount;

		let fp = ctx.framesPointer;
		let methodsMapFirstPos = ctx.methodsMapFirstPos;

		let children = 0;
		for (let child = 0; child < theOnlyChunksCount; child++) {
			const storagePos = frames[fp++];
			const count = frames[fp++];
			const size = storageSizes[storagePos];
			if (size === level) {
				continue;
			}
			const methodId = storage[storagePos + level];

			if ((methodsBuffer[methodId] += count) === count) {
				nextFrames[--methodsMapFirstPos] = methodId;
			}
			methodsBufferTheOnlyChunks[methodId]++;

			children++;
		}

		ctx.framesPointer = fp;
		ctx.methodsMapFirstPos = methodsMapFirstPos;

		return children;
	}

	function prepareBaseTheOnlyChunks(ctx) {
		const frames = ctx.frames;
		const level = ctx.level;
		const storage = ctx.storage;
		const storageSizes = ctx.storageSizes;
		const methodsBuffer = ctx.methodsBuffer;
		const baseMethodsBuffer = ctx.baseMethodsBuffer;
		const baseMethodsBufferTheOnlyChunks = ctx.baseMethodsBufferTheOnlyChunks;

		const baseTheOnlyChunksCount = ctx.baseTheOnlyChunksCount;

		const levelPlusOne = level + 1;

		let fp = ctx.framesPointer;

		for (let child = 0; child < baseTheOnlyChunksCount; child++) {
			const storagePos = frames[fp++];
			const count = frames[fp++];
			const size = storageSizes[storagePos];
			if (size === level) {
				continue;
			}
			const methodId = storage[storagePos + size - levelPlusOne];

			if (methodsBuffer[methodId] !== 0) {
				baseMethodsBuffer[methodId] += count;
				baseMethodsBufferTheOnlyChunks[methodId]++;
			}
		}

		ctx.framesPointer = fp;
	}

	function prepareBaseTheOnlyChunksReversed(ctx) {
		const frames = ctx.frames;
		const level = ctx.level;
		const storage = ctx.storage;
		const storageSizes = ctx.storageSizes;
		const methodsBuffer = ctx.methodsBuffer;
		const baseMethodsBuffer = ctx.baseMethodsBuffer;
		const baseMethodsBufferTheOnlyChunks = ctx.baseMethodsBufferTheOnlyChunks;

		const baseTheOnlyChunksCount = ctx.baseTheOnlyChunksCount;

		let fp = ctx.framesPointer;

		for (let child = 0; child < baseTheOnlyChunksCount; child++) {
			const storagePos = frames[fp++];
			const count = frames[fp++];
			const size = storageSizes[storagePos];
			if (size === level) {
				continue;
			}
			const methodId = storage[storagePos + level];

			if (methodsBuffer[methodId] !== 0) {
				baseMethodsBuffer[methodId] += count;
				baseMethodsBufferTheOnlyChunks[methodId]++;
			}
		}

		ctx.framesPointer = fp;
	}

	function prepareLongSamplesChunks(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const longSamplesBuffer = ctx.longSamplesBuffer;
		const level = ctx.level;
		const chunksToStorage = ctx.chunksToStorage;
		const storage = ctx.storage;
		const sampleToChunkEnds = ctx.sampleToChunkEnds;
		const storageSizes = ctx.storageSizes;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferLongSamples = ctx.methodsBufferLongSamples;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		const longSamplesChunksCount = ctx.longSamplesChunksCount;

		const levelPlusOne = level + 1;
		let children = 0;

		let fp = ctx.framesPointer;
		let methodsMapFirstPos = ctx.methodsMapFirstPos;

		for (let child = 0; child < longSamplesChunksCount; child++) {
			const storagePos = frames[fp++];
			const count = frames[fp++];
			const longSamplesBufferPos = frames[fp++];
			const size = storageSizes[storagePos];
			if (size === level) {
				for (let child = longSamplesBufferPos + count - 1; child >= longSamplesBufferPos; child--) {
					const sampleIndex = longSamplesBuffer[child];

					const chunkPos = (sampleToChunkEnds[sampleIndex - 1] || 0) + 1;

					const storagePos = chunksToStorage[chunkPos];
					const methodId = storage[storagePos + storageSizes[storagePos] - 1];

					if (methodsBuffer[methodId]++ === 0) {
						nextFrames[--methodsMapFirstPos] = methodId;
					}

					methodsBufferDirectSamples[methodId]++;
					children++;
				}
				continue;
			}
			const methodId = storage[storagePos + size - levelPlusOne];

			if ((methodsBuffer[methodId] += count) === count) {
				nextFrames[--methodsMapFirstPos] = methodId;
			}
			methodsBufferLongSamples[methodId]++;

			children++;
		}

		ctx.framesPointer = fp;
		ctx.methodsMapFirstPos = methodsMapFirstPos;

		return children;
	}

	function prepareDirectSamples(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const level = ctx.level;
		const chunksToStorage = ctx.chunksToStorage;
		const storage = ctx.storage;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		const directSamplesCount = ctx.directSamplesCount;

		let children = 0;

		let fp = ctx.framesPointer;
		let methodsMapFirstPos = ctx.methodsMapFirstPos;

		for (let child = 0; child < directSamplesCount; child++) {
			const sampleIndex = frames[fp++];
			const storageDelta = sampleToStorageDelta[sampleIndex] - level;
			if (storageDelta <= 0) {
				continue;
			}

			const chunkPos = sampleToChunkPos[sampleIndex];
			const storageStartPos = chunksToStorage[chunkPos];

			const storagePos = storageStartPos + storageDelta - 1;

			const methodId = storage[storagePos];

			if (methodsBuffer[methodId]++ === 0) {
				nextFrames[--methodsMapFirstPos] = methodId;
			}
			methodsBufferDirectSamples[methodId]++;

			children++;
		}

		ctx.framesPointer = fp;
		ctx.methodsMapFirstPos = methodsMapFirstPos;

		return children;
	}

	function prepareDirectSamplesDiff(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const level = ctx.level;
		const chunksToStorage = ctx.chunksToStorage;
		const storage = ctx.storage;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		const directSamplesCount = ctx.directSamplesCount;

		let children = 0;

		let fp = ctx.framesPointer;
		let methodsMapFirstPos = ctx.methodsMapFirstPos;

		for (let child = 0; child < directSamplesCount; child++) {
			const sampleIndex = frames[fp++];
			const storageDelta = (sampleToStorageDelta[sampleIndex] & 0x7fffffff) - level;
			if (storageDelta <= 0) {
				continue;
			}

			const chunkPos = sampleToChunkPos[sampleIndex]
			const storageStartPos = chunksToStorage[chunkPos];

			const storagePos = storageStartPos + storageDelta - 1;

			const methodId = storage[storagePos];

			if (methodsBuffer[methodId]++ === 0) {
				nextFrames[--methodsMapFirstPos] = methodId;
			}
			methodsBufferDirectSamples[methodId]++;

			children++;
		}

		ctx.framesPointer = fp;
		ctx.methodsMapFirstPos = methodsMapFirstPos;

		return children;
	}

	function prepareDirectSamplesReversed(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const level = ctx.level;
		const chunksToStorage = ctx.chunksToStorage;
		const storageSizes = ctx.storageSizes;
		const storage = ctx.storage;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		const directSamplesCount = ctx.directSamplesCount;

		let children = 0;

		let fp = ctx.framesPointer;
		let methodsMapFirstPos = ctx.methodsMapFirstPos;

		for (let child = 0; child < directSamplesCount; child++) {
			const sampleIndex = frames[fp++];
			const storageDelta = level - sampleToStorageDelta[sampleIndex];

			const chunkPos = sampleToChunkPos[sampleIndex];
			const storageStartPos = chunksToStorage[chunkPos];
			if (storageSizes[storageStartPos] <= storageDelta) {
				continue;
			}

			const methodId = storage[storageStartPos + storageDelta];

			if (methodsBuffer[methodId]++ === 0) {
				nextFrames[--methodsMapFirstPos] = methodId;
			}
			methodsBufferDirectSamples[methodId]++;

			children++;
		}

		ctx.framesPointer = fp;
		ctx.methodsMapFirstPos = methodsMapFirstPos;

		return children;
	}

	function prepareDirectSamplesReversedDiff(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const level = ctx.level;
		const chunksToStorage = ctx.chunksToStorage;
		const storageSizes = ctx.storageSizes;
		const storage = ctx.storage;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		const directSamplesCount = ctx.directSamplesCount;

		let children = 0;

		let fp = ctx.framesPointer;
		let methodsMapFirstPos = ctx.methodsMapFirstPos;

		for (let child = 0; child < directSamplesCount; child++) {
			const sampleIndex = frames[fp++];
			const storageDelta = level - (sampleToStorageDelta[sampleIndex] & 0x7fffffff);

			const chunkPos = sampleToChunkPos[sampleIndex];
			const storageStartPos = chunksToStorage[chunkPos];
			if (storageSizes[storageStartPos] <= storageDelta) {
				continue;
			}

			const methodId = storage[storageStartPos + storageDelta];

			if (methodsBuffer[methodId]++ === 0) {
				nextFrames[--methodsMapFirstPos] = methodId;
			}
			methodsBufferDirectSamples[methodId]++;

			children++;
		}

		ctx.framesPointer = fp;
		ctx.methodsMapFirstPos = methodsMapFirstPos;

		return children;
	}

	function prepareBaseDirectSamples(ctx) {
		const frames = ctx.frames;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const level = ctx.level;
		const chunksToStorage = ctx.chunksToStorage;
		const storage = ctx.storage;
		const methodsBuffer = ctx.methodsBuffer;
		const baseMethodsBuffer = ctx.baseMethodsBuffer;
		const baseMethodsBufferDirectSamples = ctx.baseMethodsBufferDirectSamples;

		const baseDirectSamplesCount = ctx.baseDirectSamplesCount;

		let fp = ctx.framesPointer;

		for (let child = 0; child < baseDirectSamplesCount; child++) {
			const sampleIndex = frames[fp++];
			const storageDelta = (sampleToStorageDelta[sampleIndex] & 0x7fffffff) - level;
			if (storageDelta <= 0) {
				continue;
			}

			const chunkPos = sampleToChunkPos[sampleIndex];
			const storageStartPos = chunksToStorage[chunkPos];

			const storagePos = storageStartPos + storageDelta - 1;

			const methodId = storage[storagePos];

			if (methodsBuffer[methodId] !== 0) {
				baseMethodsBuffer[methodId]++;
				baseMethodsBufferDirectSamples[methodId]++;
			}
		}

		ctx.framesPointer = fp;
	}

	function prepareBaseDirectSamplesReversed(ctx) {
		const frames = ctx.frames;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const level = ctx.level;
		const chunksToStorage = ctx.chunksToStorage;
		const storage = ctx.storage;
		const storageSizes = ctx.storageSizes;
		const methodsBuffer = ctx.methodsBuffer;
		const baseMethodsBuffer = ctx.baseMethodsBuffer;
		const baseMethodsBufferDirectSamples = ctx.baseMethodsBufferDirectSamples;

		const baseDirectSamplesCount = ctx.baseDirectSamplesCount;

		let fp = ctx.framesPointer;

		for (let child = 0; child < baseDirectSamplesCount; child++) {
			const sampleIndex = frames[fp++];
			const storageDelta = level - (sampleToStorageDelta[sampleIndex] & 0x7fffffff);

			const chunkPos = sampleToChunkPos[sampleIndex];
			const storageStartPos = chunksToStorage[chunkPos];
			if (storageSizes[storageStartPos] <= storageDelta) {
				continue;
			}

			const methodId = storage[storageStartPos + storageDelta];
			if (methodsBuffer[methodId] !== 0) {
				baseMethodsBuffer[methodId]++;
				baseMethodsBufferDirectSamples[methodId]++;
			}
		}

		ctx.framesPointer = fp;
	}

	function sortFramesForWrite(ctx) {
		const nextFrames = ctx.nextFrames;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsMapFirstPos = ctx.methodsMapFirstPos;

		let maxCount = 0;
		for (let i = methodsMapFirstPos; i < nextFrames.length; i++) {
			maxCount = Math.max(maxCount, methodsBuffer[nextFrames[i]]);
		}

		sort(nextFrames, methodsMapFirstPos, nextFrames.length, methodsBuffer, 1 << (31 - Math.clz32(maxCount)));
	}

	function writeNextFrames(ctx) {
		const nextFrames = ctx.nextFrames;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferTheOnlyChunks = ctx.methodsBufferTheOnlyChunks;
		const methodsBufferLongSamples = ctx.methodsBufferLongSamples;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		const methodsMapFirstPos = ctx.methodsMapFirstPos;

		let np = ctx.nextFramesPointer;
		let x = ctx.startX;

		for (let i = methodsMapFirstPos; i < nextFrames.length; i++) {
			const methodId = nextFrames[i];
			const count = methodsBuffer[methodId];
			const theOnlyChunksCountForMethod = methodsBufferTheOnlyChunks[methodId];
			const longSamplesChunksCountForMethod = methodsBufferLongSamples[methodId];
			const directSamplesCount = methodsBufferDirectSamples[methodId];

			nextFrames[np++] = methodId;
			nextFrames[np++] = x;
			nextFrames[np++] = count;
			nextFrames[np++] = theOnlyChunksCountForMethod;
			nextFrames[np++] = longSamplesChunksCountForMethod;
			nextFrames[np++] = directSamplesCount;

			x += count;
			methodsBufferTheOnlyChunks[methodId] = np;
			np += theOnlyChunksCountForMethod * 2;
			methodsBufferLongSamples[methodId] = np;
			np += longSamplesChunksCountForMethod * 3;
			methodsBufferDirectSamples[methodId] = np;
			np += directSamplesCount;
		}
	}

	function writeNextFramesReversed(ctx) {
		const nextFrames = ctx.nextFrames;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferTheOnlyChunks = ctx.methodsBufferTheOnlyChunks;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		const methodsMapFirstPos = ctx.methodsMapFirstPos;

		let np = ctx.nextFramesPointer;
		let x = ctx.startX;

		for (let i = methodsMapFirstPos; i < nextFrames.length; i++) {
			const methodId = nextFrames[i];
			const count = methodsBuffer[methodId];
			const theOnlyChunksCountForMethod = methodsBufferTheOnlyChunks[methodId];
			const directSamplesCount = methodsBufferDirectSamples[methodId];

			nextFrames[np++] = methodId;
			nextFrames[np++] = x;
			nextFrames[np++] = count;
			nextFrames[np++] = theOnlyChunksCountForMethod;
			nextFrames[np++] = directSamplesCount;

			x += count;
			methodsBufferTheOnlyChunks[methodId] = np;
			np += theOnlyChunksCountForMethod * 2;
			methodsBufferDirectSamples[methodId] = np;
			np += directSamplesCount;
		}
	}

	function writeNextFramesDiff(ctx) {
		const nextFrames = ctx.nextFrames;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferTheOnlyChunks = ctx.methodsBufferTheOnlyChunks;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;
		const baseMethodsBuffer = ctx.baseMethodsBuffer;
		const baseMethodsBufferTheOnlyChunks = ctx.baseMethodsBufferTheOnlyChunks;
		const baseMethodsBufferDirectSamples = ctx.baseMethodsBufferDirectSamples;

		const methodsMapFirstPos = ctx.methodsMapFirstPos;

		let np = ctx.nextFramesPointer;
		let x = ctx.startX;

		for (let i = methodsMapFirstPos; i < nextFrames.length; i++) {
			const methodId = nextFrames[i];
			const count = methodsBuffer[methodId];
			const theOnlyChunksCount = methodsBufferTheOnlyChunks[methodId];
			const directSamplesCount = methodsBufferDirectSamples[methodId];

			const baseCount = baseMethodsBuffer[methodId];
			const baseTheOnlyChunksCount = baseMethodsBufferTheOnlyChunks[methodId];
			const baseDirectSamplesCount = baseMethodsBufferDirectSamples[methodId];

			nextFrames[np++] = methodId;
			nextFrames[np++] = x;

			nextFrames[np++] = count;
			nextFrames[np++] = theOnlyChunksCount;
			nextFrames[np++] = directSamplesCount;

			nextFrames[np++] = baseCount;
			nextFrames[np++] = baseTheOnlyChunksCount;
			nextFrames[np++] = baseDirectSamplesCount;

			x += count;

			methodsBufferTheOnlyChunks[methodId] = np;
			np += theOnlyChunksCount * 2;

			methodsBufferDirectSamples[methodId] = np;
			np += directSamplesCount;

			baseMethodsBufferTheOnlyChunks[methodId] = np;
			np += baseTheOnlyChunksCount * 2;

			baseMethodsBufferDirectSamples[methodId] = np;
			np += baseDirectSamplesCount;
		}
	}

	function splitTheOnlyChunks(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const level = ctx.level;
		const storage = ctx.storage;
		const storageSizes = ctx.storageSizes;
		const methodsBufferTheOnlyChunks = ctx.methodsBufferTheOnlyChunks;

		const theOnlyChunksCount = ctx.theOnlyChunksCount;
		const levelPlusOne = level + 1;

		let fp = ctx.framesPointer;
		for (let child = 0; child < theOnlyChunksCount; child++) {
			const storagePos = frames[fp++];
			const count = frames[fp++];
			const size = storageSizes[storagePos];
			if (size === level) {
				continue;
			}

			const methodId = storage[storagePos + size - levelPlusOne];

			nextFrames[methodsBufferTheOnlyChunks[methodId]++] = storagePos;
			nextFrames[methodsBufferTheOnlyChunks[methodId]++] = count;
		}
		ctx.framesPointer = fp;
	}

	function splitTheOnlyChunksReversed(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const level = ctx.level;
		const storage = ctx.storage;
		const storageSizes = ctx.storageSizes;
		const methodsBufferTheOnlyChunks = ctx.methodsBufferTheOnlyChunks;

		const theOnlyChunksCount = ctx.theOnlyChunksCount;

		let fp = ctx.framesPointer;
		for (let child = 0; child < theOnlyChunksCount; child++) {
			const storagePos = frames[fp++];
			const count = frames[fp++];
			const size = storageSizes[storagePos];
			if (size === level) {
				continue;
			}

			const methodId = storage[storagePos + level];

			nextFrames[methodsBufferTheOnlyChunks[methodId]++] = storagePos;
			nextFrames[methodsBufferTheOnlyChunks[methodId]++] = count;
		}
		ctx.framesPointer = fp;
	}

	function splitBaseTheOnlyChunks(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const level = ctx.level;
		const storage = ctx.storage;
		const storageSizes = ctx.storageSizes;
		const methodsBuffer = ctx.methodsBuffer;
		const baseMethodsBufferTheOnlyChunks = ctx.baseMethodsBufferTheOnlyChunks;

		const baseTheOnlyChunksCount = ctx.baseTheOnlyChunksCount;
		const levelPlusOne = level + 1;

		let fp = ctx.framesPointer;
		for (let child = 0; child < baseTheOnlyChunksCount; child++) {
			const storagePos = frames[fp++];
			const count = frames[fp++];
			const size = storageSizes[storagePos];
			if (size === level) {
				continue;
			}

			const methodId = storage[storagePos + size - levelPlusOne];

			if (methodsBuffer[methodId] !== 0) {
				nextFrames[baseMethodsBufferTheOnlyChunks[methodId]++] = storagePos;
				nextFrames[baseMethodsBufferTheOnlyChunks[methodId]++] = count;
			}
		}
		ctx.framesPointer = fp;
	}

	function splitBaseTheOnlyChunksReversed(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const level = ctx.level;
		const storage = ctx.storage;
		const storageSizes = ctx.storageSizes;
		const methodsBuffer = ctx.methodsBuffer;
		const baseMethodsBufferTheOnlyChunks = ctx.baseMethodsBufferTheOnlyChunks;

		const baseTheOnlyChunksCount = ctx.baseTheOnlyChunksCount;

		let fp = ctx.framesPointer;
		for (let child = 0; child < baseTheOnlyChunksCount; child++) {
			const storagePos = frames[fp++];
			const count = frames[fp++];
			const size = storageSizes[storagePos];
			if (size === level) {
				continue;
			}

			const methodId = storage[storagePos + level];

			if (methodsBuffer[methodId] !== 0) {
				nextFrames[baseMethodsBufferTheOnlyChunks[methodId]++] = storagePos;
				nextFrames[baseMethodsBufferTheOnlyChunks[methodId]++] = count;
			}
		}
		ctx.framesPointer = fp;
	}

	function splitLongSamplesChunks(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const longSamplesBuffer = ctx.longSamplesBuffer;
		const level = ctx.level;
		const chunksToStorage = ctx.chunksToStorage;
		const storage = ctx.storage;
		const sampleToChunkEnds = ctx.sampleToChunkEnds;
		const storageSizes = ctx.storageSizes;
		const methodsBufferLongSamples = ctx.methodsBufferLongSamples;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		const longSamplesChunksCount = ctx.longSamplesChunksCount;

		const levelPlusOne = level + 1;

		let fp = ctx.framesPointer;
		for (let child = 0; child < longSamplesChunksCount; child++) {
			const storagePos = frames[fp++];
			const count = frames[fp++];
			const longSamplesBufferPos = frames[fp++];
			const size = storageSizes[storagePos];
			if (size === level) {
				for (let child = longSamplesBufferPos + count - 1; child >= longSamplesBufferPos; child--) {
					const sampleIndex = longSamplesBuffer[child];

					let chunkPos = (sampleToChunkEnds[sampleIndex - 1] || 0) + 1;
					const storagePos = chunksToStorage[chunkPos];
					const storageSize = storageSizes[storagePos];
					sampleToChunkPos[sampleIndex] = chunkPos;
					sampleToStorageDelta[sampleIndex] = storageSize + levelPlusOne;
					let storageDelta = sampleToStorageDelta[sampleIndex] - levelPlusOne;
					if (storageDelta === 1) {
						const chunkEnd = sampleToChunkEnds[sampleIndex] || chunksToStorage.length;
						if (++chunkPos !== chunkEnd) {
							sampleToChunkPos[sampleIndex] = chunkPos;
							const storagePos = chunksToStorage[chunkPos];
							storageDelta = storageSizes[storagePos];
							sampleToStorageDelta[sampleIndex] = storageDelta + levelPlusOne;
						}
					}

					const methodId = storage[storagePos + storageSize - 1];

					nextFrames[methodsBufferDirectSamples[methodId]++] = sampleIndex;
				}
				continue;
			}

			const methodId = storage[storagePos + size - levelPlusOne];

			nextFrames[methodsBufferLongSamples[methodId]++] = storagePos;
			nextFrames[methodsBufferLongSamples[methodId]++] = count;
			nextFrames[methodsBufferLongSamples[methodId]++] = longSamplesBufferPos;
		}
		ctx.framesPointer = fp;
	}

	function splitDirectSamples(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const level = ctx.level;
		const chunksToStorage = ctx.chunksToStorage;
		const storage = ctx.storage;
		const sampleToChunkEnds = ctx.sampleToChunkEnds;
		const storageSizes = ctx.storageSizes;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		const directSamplesCount = ctx.directSamplesCount;

		const levelPlusOne = level + 1;

		let fp = ctx.framesPointer;
		for (let child = 0; child < directSamplesCount; child++) {
			const sampleIndex = frames[fp++];
			let storageDelta = sampleToStorageDelta[sampleIndex] - level;
			if (storageDelta <= 0) {
				continue;
			}
			let chunkPos = sampleToChunkPos[sampleIndex];
			const storageStartPos = chunksToStorage[chunkPos];

			const storagePos = storageStartPos + storageDelta - 1;

			if (storageDelta === 1) {
				const chunkEnd = sampleToChunkEnds[sampleIndex] || chunksToStorage.length;
				if (++chunkPos !== chunkEnd) {
					sampleToChunkPos[sampleIndex] = chunkPos;
					const storagePos = chunksToStorage[chunkPos];
					storageDelta = storageSizes[storagePos];
					sampleToStorageDelta[sampleIndex] = storageDelta + levelPlusOne;
				}
			}

			const methodId = storage[storagePos];

			nextFrames[methodsBufferDirectSamples[methodId]++] = sampleIndex;
		}
		ctx.framesPointer = fp;
	}

	function splitDirectSamplesDiff(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const level = ctx.level;
		const storage = ctx.storage;
		const storageSizes = ctx.storageSizes;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const chunksToStorage = ctx.chunksToStorage;
		const sampleToChunkEnds = ctx.sampleToChunkEnds;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		const directSamplesCount = ctx.directSamplesCount;
		const levelPlusOne = level + 1;

		let fp = ctx.framesPointer;

		for (let child = 0; child < directSamplesCount; child++) {
			const sampleIndex = frames[fp++];
			let storageDelta = (sampleToStorageDelta[sampleIndex] & 0x7fffffff) - level;
			if (storageDelta <= 0) {
				sampleToStorageDelta[sampleIndex] = 0;
				continue;
			}
			let chunkPos = sampleToChunkPos[sampleIndex];
			const storageStartPos = chunksToStorage[chunkPos];

			const storagePos = storageStartPos + storageDelta - 1;

			if (storageDelta === 1) {
				const chunkEnd = sampleToChunkEnds[sampleIndex] || chunksToStorage.length;
				if (++chunkPos !== chunkEnd) {
					sampleToChunkPos[sampleIndex] = chunkPos;
					const storagePos = chunksToStorage[chunkPos];
					storageDelta = storageSizes[storagePos];
					sampleToStorageDelta[sampleIndex] = (storageDelta + levelPlusOne) | 0x80000000;
				}
			}

			const methodId = storage[storagePos];

			nextFrames[methodsBufferDirectSamples[methodId]++] = sampleIndex;
		}
		ctx.framesPointer = fp;
	}

	function splitDirectSamplesReversed(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const level = ctx.level;
		const chunksToStorage = ctx.chunksToStorage;
		const storage = ctx.storage;
		const sampleToChunkEnds = ctx.sampleToChunkEnds;
		const storageSizes = ctx.storageSizes;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		const directSamplesCount = ctx.directSamplesCount;

		const levelPlusOne = level + 1;

		let fp = ctx.framesPointer;
		for (let child = 0; child < directSamplesCount; child++) {
			const sampleIndex = frames[fp++];
			const storageDelta = level - sampleToStorageDelta[sampleIndex];

			let chunkPos = sampleToChunkPos[sampleIndex];
			const storageStartPos = chunksToStorage[chunkPos];
			const storageSize = storageSizes[storageStartPos];
			if (storageSize <= storageDelta) {
				continue;
			}

			const storagePos = storageStartPos + storageDelta;

			if (storageDelta + 1 === storageSize) {
				const chunkStart = sampleToChunkEnds[sampleIndex - 1] || 0;
				if (--chunkPos >= chunkStart) {
					sampleToChunkPos[sampleIndex] = chunkPos;
					sampleToStorageDelta[sampleIndex] = levelPlusOne;
				}
			}

			const methodId = storage[storagePos];

			nextFrames[methodsBufferDirectSamples[methodId]++] = sampleIndex;
		}
		ctx.framesPointer = fp;
	}

	function splitDirectSamplesReversedDiff(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const level = ctx.level;
		const storage = ctx.storage;
		const storageSizes = ctx.storageSizes;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const chunksToStorage = ctx.chunksToStorage;
		const sampleToChunkEnds = ctx.sampleToChunkEnds;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		const directSamplesCount = ctx.directSamplesCount;
		const levelPlusOne = level + 1;

		let fp = ctx.framesPointer;

		for (let child = 0; child < directSamplesCount; child++) {
			const sampleIndex = frames[fp++];
			const delta = sampleToStorageDelta[sampleIndex] & 0x7fffffff;
			const storageDelta = level - delta;

			let chunkPos = sampleToChunkPos[sampleIndex];
			const storageStartPos = chunksToStorage[chunkPos];
			const storageSize = storageSizes[storageStartPos];
			if (storageSize <= storageDelta) {
				sampleToStorageDelta[sampleIndex] = delta;
				continue;
			}

			const storagePos = storageStartPos + storageDelta;

			if (storageDelta + 1 === storageSize) {
				const chunkStart = sampleToChunkEnds[sampleIndex - 1] || 0;
				if (--chunkPos >= chunkStart) {
					sampleToChunkPos[sampleIndex] = chunkPos;
					sampleToStorageDelta[sampleIndex] = levelPlusOne | 0x80000000;
				}
			}

			const methodId = storage[storagePos];

			nextFrames[methodsBufferDirectSamples[methodId]++] = sampleIndex;
		}
		ctx.framesPointer = fp;
	}

	function splitBaseDirectSamples(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const level = ctx.level;
		const chunksToStorage = ctx.chunksToStorage;
		const storage = ctx.storage;
		const sampleToChunkEnds = ctx.sampleToChunkEnds;
		const storageSizes = ctx.storageSizes;
		const methodsBuffer = ctx.methodsBuffer;
		const baseMethodsBufferDirectSamples = ctx.baseMethodsBufferDirectSamples;

		const baseDirectSamplesCount = ctx.baseDirectSamplesCount;

		const levelPlusOne = level + 1;

		let fp = ctx.framesPointer;

		for (let child = 0; child < baseDirectSamplesCount; child++) {
			const sampleIndex = frames[fp++];
			const baseStorageDelta = sampleToStorageDelta[sampleIndex];
			let storageDelta = (baseStorageDelta & 0x7fffffff) - level;
			if (storageDelta <= 0) {
				continue;
			}

			let chunkPos = sampleToChunkPos[sampleIndex];
			const storageStartPos = chunksToStorage[chunkPos];

			const storagePos = storageStartPos + storageDelta - 1;

			if (storageDelta === 1 &&
				// will be done for this sample by not-based step
				(baseStorageDelta & 0x80000000) === 0) {
				const chunkEnd = sampleToChunkEnds[sampleIndex] || chunksToStorage.length;
				if (++chunkPos !== chunkEnd) {
					sampleToChunkPos[sampleIndex] = chunkPos;
					const storagePos = chunksToStorage[chunkPos];
					storageDelta = storageSizes[storagePos];
					sampleToStorageDelta[sampleIndex] = storageDelta + levelPlusOne;
				}
			}

			const methodId = storage[storagePos];

			if (methodsBuffer[methodId] !== 0) {
				nextFrames[baseMethodsBufferDirectSamples[methodId]++] = sampleIndex;
			}
		}
		ctx.framesPointer = fp;
	}

	function splitBaseDirectSamplesReversed(ctx) {
		const frames = ctx.frames;
		const nextFrames = ctx.nextFrames;
		const sampleToChunkPos = ctx.sampleToChunkPos;
		const sampleToStorageDelta = ctx.sampleToStorageDelta;
		const level = ctx.level;
		const chunksToStorage = ctx.chunksToStorage;
		const storage = ctx.storage;
		const sampleToChunkEnds = ctx.sampleToChunkEnds;
		const storageSizes = ctx.storageSizes;
		const methodsBuffer = ctx.methodsBuffer;
		const baseMethodsBufferDirectSamples = ctx.baseMethodsBufferDirectSamples;

		const baseDirectSamplesCount = ctx.baseDirectSamplesCount;

		const levelPlusOne = level + 1;

		let fp = ctx.framesPointer;

		for (let child = 0; child < baseDirectSamplesCount; child++) {
			const sampleIndex = frames[fp++];
			const baseStorageDelta = sampleToStorageDelta[sampleIndex];
			const storageDelta = level - (baseStorageDelta & 0x7fffffff);

			let chunkPos = sampleToChunkPos[sampleIndex];
			const storageStartPos = chunksToStorage[chunkPos];
			const storageSize = storageSizes[storageStartPos];
			if (storageSize <= storageDelta) {
				continue;
			}

			const storagePos = storageStartPos + storageDelta;

			if (storageDelta + 1 === storageSize &&
				// will be done for this sample by not-based step
				(baseStorageDelta & 0x80000000) === 0) {
				const chunkStart = sampleToChunkEnds[sampleIndex - 1] || 0;
				if (--chunkPos >= chunkStart) {
					sampleToChunkPos[sampleIndex] = chunkPos;
					sampleToStorageDelta[sampleIndex] = levelPlusOne;
				}
			}

			const methodId = storage[storagePos];

			if (methodsBuffer[methodId] !== 0) {
				nextFrames[baseMethodsBufferDirectSamples[methodId]++] = sampleIndex;
			}
		}
		ctx.framesPointer = fp;
	}

	function cleanupMethodBuffers(ctx) {
		const nextFrames = ctx.nextFrames;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferTheOnlyChunks = ctx.methodsBufferTheOnlyChunks;
		const methodsBufferLongSamples = ctx.methodsBufferLongSamples;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		let np = ctx.nextFramesPointer;
		let methodsMapFirstPos = ctx.methodsMapFirstPos;
		for (let i = methodsMapFirstPos; i < nextFrames.length; i++) {
			const methodId = nextFrames[np];

			methodsBuffer[methodId] = 0;
			methodsBufferTheOnlyChunks[methodId] = 0;
			methodsBufferLongSamples[methodId] = 0;
			methodsBufferDirectSamples[methodId] = 0;

			np += nextFrames[np + 3] * 2 + nextFrames[np + 4] * 3 + nextFrames[np + 5] + 6;
		}
		ctx.nextFramesPointer = np;
	}

	function cleanupMethodBuffersReversed(ctx) {
		const nextFrames = ctx.nextFrames;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferTheOnlyChunks = ctx.methodsBufferTheOnlyChunks;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;

		let np = ctx.nextFramesPointer;
		let methodsMapFirstPos = ctx.methodsMapFirstPos;
		for (let i = methodsMapFirstPos; i < nextFrames.length; i++) {
			const methodId = nextFrames[np];

			methodsBuffer[methodId] = 0;
			methodsBufferTheOnlyChunks[methodId] = 0;
			methodsBufferDirectSamples[methodId] = 0;

			np += nextFrames[np + 3] * 2 + nextFrames[np + 4] + 5;
		}
		ctx.nextFramesPointer = np;
	}

	function cleanupMethodBuffersDiff(ctx) {
		const nextFrames = ctx.nextFrames;
		const methodsBuffer = ctx.methodsBuffer;
		const methodsBufferTheOnlyChunks = ctx.methodsBufferTheOnlyChunks;
		const methodsBufferDirectSamples = ctx.methodsBufferDirectSamples;
		const baseMethodsBuffer = ctx.baseMethodsBuffer;
		const baseMethodsBufferTheOnlyChunks = ctx.baseMethodsBufferTheOnlyChunks;
		const baseMethodsBufferDirectSamples = ctx.baseMethodsBufferDirectSamples;

		let np = ctx.nextFramesPointer;
		let methodsMapFirstPos = ctx.methodsMapFirstPos;
		for (let i = methodsMapFirstPos; i < nextFrames.length; i++) {
			const methodId = nextFrames[np];

			methodsBuffer[methodId] = 0;
			methodsBufferTheOnlyChunks[methodId] = 0;
			methodsBufferDirectSamples[methodId] = 0;

			baseMethodsBuffer[methodId] = 0;
			baseMethodsBufferTheOnlyChunks[methodId] = 0;
			baseMethodsBufferDirectSamples[methodId] = 0;

			np += (nextFrames[np + 3] + nextFrames[np + 6]) * 2 + (nextFrames[np + 4] + nextFrames[np + 7]) + 8;
		}
		ctx.nextFramesPointer = np;
	}

	function renderWithMatch(newCanvasHeight, root, level, minLevel) {
		newCanvasHeight = Math.min(16384 / (devicePixelRatio || 1), newCanvasHeight) | 0;
		if (canvasHeight !== newCanvasHeight) {
			canvasHeight = newCanvasHeight;
			canvas.style.height = canvasHeight + 'px';
			canvas.height = canvasHeight * (devicePixelRatio || 1);
			c = canvas.getContext('2d');
			if (devicePixelRatio) c.scale(devicePixelRatio, devicePixelRatio);
			c.font = document.body.style.font;
		}
		const matched = render(root, level, minLevel);
		displayMatched(matched);
	}

	function redrawHeatSamplesImpl(from, to, dx) {
		const startBlock = currentHeatmap.startBlock();

		const scale = currentHeatmap.timeSquareScale;
		const max = currentHeatmap.max(showFound);
		const min = currentHeatmap.min(showFound);
		const delta = Math.max(1, max - min);
		const counts = showFound ? currentHeatmap.blocksData.foundCounts : currentHeatmap.blocksData.counts;

		const r = showFound ? dRs : dR;
		const g = showFound ? dGs : dG;
		const b = showFound ? dBs : dB;

		for (let index = from; index <= to; index++) {
			let color;
			const firstCountsIndex = (index - startBlock) * scale;
			const afterLastCountsIndex = firstCountsIndex + scale;
			if (afterLastCountsIndex <= 0 || firstCountsIndex >= counts.length) {
				color = '#ddd';
			} else {
				const countBeforeFirstBlock = getCount(counts, firstCountsIndex - 1);
				const countAtLastBlock = getCount(counts, afterLastCountsIndex - 1);
				const count = countAtLastBlock - countBeforeFirstBlock;
				if (count === 0) {
					color = '#fff';
				} else {
					let ratio = Math.max(0, (count - min) / delta);
					const ratioM = (ratio * 192 + 24) | 0;

					const C = 255 - Math.abs(255 - (ratioM << 1));
					const m = (255 - ratioM) - (C >> 1);
					color = 'rgb(' + (((r * C) >> 8) + m) + ',' + (((g * C) >> 8) + m) + ',' + (((b * C) >> 8) + m) + ')';
				}
			}
			heatC.fillStyle = color;

			let x = Math.floor(index / currentHeatmap.timeSquareRowsCount);
			let y = index % currentHeatmap.timeSquareRowsCount;
			heatC.fillRect(x * sq - dx, y * sq + canvasTimeHeight, sq, sq);
		}

		let fromBlock = Math.floor(from / currentHeatmap.squaresBetweenMarks / currentHeatmap.timeSquareRowsCount) - 1;
		let toBlock = Math.ceil(to / currentHeatmap.squaresBetweenMarks / currentHeatmap.timeSquareRowsCount);

		let markPixels = currentHeatmap.squaresBetweenMarks * sq;

		heatC.fillStyle = '#fff';
		heatC.fillRect(
			fromBlock * markPixels - dx,
			0,
			(toBlock - fromBlock + 1) * markPixels,
			canvasTimeHeight
		);

		heatC.beginPath();
		heatC.moveTo(fromBlock * markPixels - dx + 0.5, canvasTimeHeight + 0.5);
		heatC.lineTo(toBlock * markPixels - dx + 0.5, canvasTimeHeight + 0.5);
		heatC.stroke();

		heatC.fillStyle = '#000';

		let groupSize = currentHeatmap.timeSquareRowsCount;
		let oneBlockMs = currentHeatmap.currentTimeSquareMs();
		let oneGroupMs = oneBlockMs * groupSize;
		let oneMarkMs = oneGroupMs * currentHeatmap.squaresBetweenMarks;
		for (let markIndex = fromBlock; markIndex <= toBlock; markIndex++) {
			let origin = Math.floor(startMs / oneMarkMs) * oneMarkMs;
			let ms = origin + markIndex * oneMarkMs;
			let markOffset = markIndex * markPixels - dx;

			let title = formatTime(ms, currentTimezone === 'UTC' ? timeOptionsTinyWithTZ : timeOptionsTiny);
			let x = markOffset + 4;
			heatC.fillText(title, x, canvasTimeHeight - 1, markPixels);

			heatC.beginPath();
			heatC.moveTo(markOffset + 0.5, canvasTimeHeight - 2 * sqScale + 0.5);
			heatC.lineTo(markOffset + 0.5, canvasTimeHeight + 2 * sqScale + 0.5);
			heatC.stroke();
		}
	}

	function redrawHeatSamples(from, to) {
		let dx = Math.floor((heatCanvasContainer.scrollLeft - canvasScrollPadding) * sqScale);

		from = Math.max(from, currentHeatmap.timeSquareRowsCount * Math.floor(dx / sq));
		to = Math.min(to, currentHeatmap.timeSquareRowsCount * Math.ceil((dx + heatCanvas.width) / sq));
		redrawHeatSamplesImpl(from, to, dx);
	}

	function redrawHeatMap() {
		redrawHeatSamples(0, (1 + currentHeatmap.count() / currentHeatmap.timeSquareRowsCount | 0) * currentHeatmap.timeSquareRowsCount);
	}

	function fillCanvasWithEvents(zoom) {
		currentHeatmap.setZoom(zoom);

		const m = devicePixelRatio || 1;
		sqScale = m;
		sqPx = 5;
		sq = Math.floor(sqPx * m);
		sqPx = sq / m;
		canvasTimeHeight = sq * 2 + 2;
		canvasTimeHeightPx = canvasTimeHeight / m;

		const widthPx = canvasScrollPadding + heatCanvasContainer.offsetWidth + canvasScrollPadding;
		const heightPx = sqPx * currentHeatmap.timeSquareRowsCount + canvasTimeHeightPx;

		const heatWidthPx = Math.max(20, Math.ceil(currentHeatmap.count() / currentHeatmap.timeSquareRowsCount)) * sqPx;
		heatCanvasWrapper.style.width = heatWidthPx + 'px';
		if (heatCanvasContainer.offsetWidth > heatWidthPx) {
			heatCanvasContainer.style.overflowX = 'hidden';
		} else {
			heatCanvasContainer.style.overflowX = 'scroll';
		}

		heatCanvas.width = Math.ceil(widthPx * m);
		heatCanvas.height = Math.ceil(heightPx * m);
		heatCanvas.style.width = widthPx + 'px';
		heatCanvas.style.height = heightPx + 'px';

		heatC = heatCanvas.getContext('2d');
		heatC.font = sq * 2 + 'px Verdana, sans-serif';
		heatC.lineWidth = 1;
		heatC.strokeStyle = '#000';

		document.getElementById('heatmap-height-line').textContent = zoomToText[currentHeatmap.zoom];
		redrawHeatMap();
	}

	function search(r, filter) {
		filter = filter || false;
		if (r && (r = prompt('Enter regexp to ' + (filter ? 'filter' : 'search') + ':', '')) === null) {
			return;
		}
		const wasFilter = filterFrames;
		filterFrames = filter;
		if (!r) {
			showFound = false;
			pattern = undefined;
			redrawHeatMap();
			if (wasFilter === filterFrames) {
				searchOnFlame();
			} else {
				reselect();
			}
			return;
		}

		const regex = RegExp(r)
		pattern = method => regex.test(methods.n[method]);
		showFound = true;
		const matchedMethods = new Set();
		for (let index = methods.c.length - 1; index >= 0; index--) {
			if (regex.test(methods.n[index])) {
				matchedMethods.add(index);
			}
		}

		const storage = currentHeatmap.blocksData.storage;
		const storageSizes = currentHeatmap.blocksData.storageSizes;
		const marks = currentHeatmap.blocksData.marks;
		marks.fill(0);
		for (let storagePos = storage.length - 1; storagePos >= 0;) {
			if (matchedMethods.has(storage[storagePos])) {
				marks[storagePos >> 5] |= 1 << (storagePos & 31);
				storagePos--;
				while (storagePos >= 0 && storageSizes[storagePos] !== 1) {
					marks[storagePos >> 5] |= 1 << (storagePos & 31);
					storagePos--;
				}
			} else {
				storagePos--;
			}
		}

		searchForMarks();
		if (filterFrames || wasFilter) {
			reselect();
		} else {
			searchOnFlame();
		}
	}

	function searchForMethod(methodId) {
		showFound = true;
		pattern = function (method) {
			return method === methodId;
		}
		const storage = currentHeatmap.blocksData.storage;
		const storageSizes = currentHeatmap.blocksData.storageSizes;
		const marks = currentHeatmap.blocksData.marks;
		marks.fill(0);
		for (let storagePos = storage.length - 1; storagePos >= 0;) {
			if (storage[storagePos] === methodId) {
				marks[storagePos >> 5] |= 1 << (storagePos & 31);
				storagePos--;
				while (storagePos >= 0 && storageSizes[storagePos] !== 1) {
					marks[storagePos >> 5] |= 1 << (storagePos & 31);
					storagePos--;
				}
			} else {
				storagePos--;
			}
		}

		searchForMarks();
		searchOnFlame();
	}

	function searchOnFlame() {
		const matched = render(root, rootLevel);
		displayMatched(matched);
	}

	function displayMatched(matched) {
		if (showFound) {
			document.getElementById('matchval').textContent = pct(matched, root.width) + '%';
			document.getElementById('match').style.display = 'inline-block';
			document.getElementById('produced').style.display = 'none';
		} else {
			document.getElementById('match').style.display = 'none';
			document.getElementById('produced').style.display = 'inherit';
		}
	}

	function searchForMarks() {
		const found = currentHeatmap.blocksData.foundCounts;
		const maxCounts = currentHeatmap.blocksData.searchMaxCounts;
		const minCounts = currentHeatmap.blocksData.searchMinCounts;

		searchForMarksStep();
		calculateMinMax(found, minCounts, maxCounts);
		redrawHeatMap();
	}

	function searchForMarksStep() {
		const counts = currentHeatmap.blocksData.counts;
		const chunksToStorage = currentHeatmap.blocksData.chunksToStorage;
		const sampleToChunkEnds = currentHeatmap.blocksData.sampleToChunkEnds;
		const found = currentHeatmap.blocksData.foundCounts;
		const marks = currentHeatmap.blocksData.marks;
		const sampleMarks = currentHeatmap.blocksData.sampleMarks;

		sampleMarks.fill(0);
		let sample = 0;
		let chunk = 0;
		let foundCount = 0;
		for (let block = 0; block < counts.length; block++) {
			const count = counts[block];
			for (; sample < count; sample++) {
				const chunkEnd = sampleToChunkEnds[sample];
				for (; chunk < chunkEnd; chunk++) {
					const storagePos = chunksToStorage[chunk];
					if ((marks[storagePos >> 5] & (1 << (storagePos & 31))) !== 0) {
						sampleMarks[sample >> 5] |= 1 << (sample & 31);
						foundCount++;
						chunk = chunkEnd;
						break;
					}
				}
			}
			found[block] = foundCount;
		}
	}

	function calculateMinMax(counts, minCounts, maxCounts) {
		for (let zi = 0; zi < zoomToGroupSize.length; zi++) {
			const z = zoomToGroupSize[zi];
			let max = 0;
			let min = 0x7fffffff;
			let prev = 0;
			for (let i = z - 1; i < counts.length; i += z) {
				const f = counts[i];
				const diff = f - prev;
				if (diff !== 0) {
					max = Math.max(max, diff);
					min = Math.min(min, diff);
				}
				prev = f;
			}
			const diff = counts[counts.length - 1] - prev;
			max = Math.max(max, diff);
			min = Math.min(min, max);
			maxCounts[zi] = max;
			minCounts[zi] = min;
		}
	}

	function calculateInitialZoom() {
		const width = Math.floor(heatCanvasContainer.offsetWidth / sq / (devicePixelRatio || 1));

		let z = 0;
		for (; z < zoomToGroupSize.length - 1; z++) {
			if (currentHeatmap.blocksData.counts.length / (zoomToGroupSize[z] * zoomToHeight[z]) < width) {
				break;
			}
		}
		return z;
	}

	function renderHeatmap() {
		let dx = heatCanvasContainer.scrollLeft - canvasScrollPadding;
		heatCanvas.style.transform = 'translate(' + dx + 'px, 0px)';

		fillCanvasWithEvents(calculateInitialZoom());

		renderWrapper(0, currentHeatmap.blocksData.counts.length - 1);
	}

	heatCanvasContainer.addEventListener('scroll', function () {
		let dx = Math.floor((heatCanvasContainer.scrollLeft - canvasScrollPadding) * sqScale);
		let dxPx = dx / sqScale;
		heatCanvas.style.transform = 'translate(' + dxPx + 'px, 0px)';
		let delta = Math.abs(dx - prevDx);
		if (delta >= heatCanvas.width) {
			redrawHeatMap();
		} else {
			heatC.drawImage(heatCanvas, prevDx - dx, 0);
			if (prevDx < dx) {
				let from = currentHeatmap.timeSquareRowsCount * Math.floor((prevDx + heatCanvas.width) / sq);
				let to = currentHeatmap.timeSquareRowsCount * Math.ceil((dx + heatCanvas.width) / sq);
				redrawHeatSamplesImpl(from, to, dx);
			} else {
				let from = currentHeatmap.timeSquareRowsCount * Math.floor(dx / sq);
				let to = currentHeatmap.timeSquareRowsCount * Math.ceil(prevDx / sq);
				redrawHeatSamplesImpl(from, to, dx);
			}
		}
		prevDx = dx;
		cooldownTime = performance.now() + 500;
	});

	document.getElementById('heatmap-height-line').onclick = function () {
		fillCanvasWithEvents((zoomToGroupSize.length + currentHeatmap.zoom - 1) % (zoomToGroupSize.length));
		highlightStart = highlightEnd = heatDiffSample1 = heatDiffSample2 = heatActiveSample1 = heatActiveSample2 = -1;
		for (let suffix of ['Selection', 'Diff', 'Active']) {
			for (let prefix of ['left', 'leftMiddle', 'middle', 'rightMiddle', 'right']) {
				document.getElementById(prefix + suffix).style.display = 'none';
			}
		}
	}

	function callHighlightRedraw(start, end, suffix) {
		let left = document.getElementById('left' + suffix);
		let leftMiddle = document.getElementById('leftMiddle' + suffix);
		let middle = document.getElementById('middle' + suffix);
		let rightMiddle = document.getElementById('rightMiddle' + suffix);
		let right = document.getElementById('right' + suffix);

		let x1 = Math.floor(start / currentHeatmap.timeSquareRowsCount);
		let y1 = start % currentHeatmap.timeSquareRowsCount;
		let x2 = Math.floor(end / currentHeatmap.timeSquareRowsCount);
		let y2 = end % currentHeatmap.timeSquareRowsCount;

		let veryStartX = x1 * sqPx;
		let veryStartY = y1 * sqPx;
		let veryEndX = x2 * sqPx;
		let px = 1 / sqScale;
		let singleElementWidth = sqPx + px;

		let topPadding = canvasTimeHeightPx;

		if (x1 === x2) {
			left.style.display = 'none';
			right.style.display = 'none';

			for (let b of [middle, leftMiddle, rightMiddle]) {
				b.style.left = veryStartX + 'px';
				b.style.top = topPadding + veryStartY + 'px';
				b.style.width = singleElementWidth + 'px';
				b.style.height = (y2 - y1 + 1) * sqPx + px + 'px';
				b.style.display = 'block';
			}
		} else {
			left.style.left = veryStartX + 'px';
			left.style.top = topPadding + veryStartY + 'px';
			left.style.width = singleElementWidth + 'px';
			left.style.height = (currentHeatmap.timeSquareRowsCount - y1) * sqPx + px + 'px';
			left.style.display = 'block';

			leftMiddle.style.left = veryStartX + 'px';
			leftMiddle.style.top = topPadding + 'px';
			leftMiddle.style.width = singleElementWidth + 'px';
			leftMiddle.style.height = y1 * sqPx + px + 'px';
			leftMiddle.style.display = 'block';

			rightMiddle.style.left = veryEndX + 'px';
			rightMiddle.style.top = topPadding + (y2 + 1) * sqPx + 'px';
			rightMiddle.style.width = sqPx + px + 'px';
			rightMiddle.style.height = (currentHeatmap.timeSquareRowsCount - y2 - 1) * sqPx + px + 'px';
			rightMiddle.style.display = 'block';

			right.style.left = veryEndX + 'px';
			right.style.top = topPadding + 'px';
			right.style.width = sqPx + px + 'px';
			right.style.height = (y2 + 1) * sqPx + px + 'px';
			right.style.display = 'block';

			if (x2 - x1 === 1) {
				middle.style.display = 'none';
			} else {
				middle.style.left = veryStartX + sqPx + px + 'px';
				middle.style.top = topPadding + 'px';
				middle.style.width = (x2 - x1 - 1) * sqPx - px + 'px';
				middle.style.height = currentHeatmap.timeSquareRowsCount * sqPx + px + 'px';
				middle.style.display = 'block';
			}
		}
	}

	function callSelectionRedraw(sample, shiftPressed, ctrlPressed) {
		if (ctrlPressed) {
			highlightStart = sample;
			highlightEnd = sample + Math.abs(heatActiveSample1 - heatActiveSample2);
		} else if (shiftPressed && heatActiveSample1 !== -1) {
			highlightStart = Math.min(heatActiveSample1, sample);
			highlightEnd = Math.max(heatActiveSample1, sample);
		} else {
			highlightStart = highlightEnd = sample;
		}

		callHighlightRedraw(highlightStart, highlightEnd, 'Selection');

		const index = (sample - currentHeatmap.startBlock()) * currentHeatmap.timeSquareScale;
		const counts = currentHeatmap.blocksData.counts;
		const foundCounts = currentHeatmap.blocksData.foundCounts;
		if (index >= 0 && index < counts.length) {
			const countBeforeFirstBlock = getCount(counts, index - 1);
			const countAtLastBlock = getCount(counts, index + currentHeatmap.timeSquareScale - 1);
			const searchCountBeforeFirstBlock = getCount(foundCounts, index - 1);
			const searchCountAtLastBlock = getCount(foundCounts, index + currentHeatmap.timeSquareScale - 1);

			const total = countAtLastBlock - countBeforeFirstBlock;
			const searchTotal = searchCountAtLastBlock - searchCountBeforeFirstBlock;
			const max = currentHeatmap.max(false);
			const totalText = "total: " + total + " (" + Math.floor(total / max * 100) + "%)";
			const searchText = showFound
				? "; match: " + searchTotal + " (" + Math.floor(searchTotal / Math.max(currentHeatmap.max(true), 1) * 100) + "%)"
				: "";
			status.textContent = totalText + searchText;
			status.style.display = 'inline';
		} else {
			status.style.display = 'none';
		}
		const ms = Math.floor(startMs / currentHeatmap.currentTimeSquareMs()) * currentHeatmap.currentTimeSquareMs() + currentHeatmap.minimalTimeSquare * index;
		heatCanvas.title = formatTime(ms, timeOptionsShort);
	}

	function callActiveRedraw(sample, shiftPressed, ctrlPressed) {
		if (ctrlPressed) {
			let minSelected = Math.min(heatActiveSample1, heatActiveSample2);
			let maxSelected = Math.max(heatActiveSample1, heatActiveSample2);

			heatDiffSample1 = highlightStart;
			heatDiffSample2 = highlightEnd;

			callHighlightRedraw(highlightStart, highlightEnd, 'Diff');
			renderWrapperDiff(
				(minSelected - currentHeatmap.startBlock()) * currentHeatmap.timeSquareScale,
				(maxSelected - currentHeatmap.startBlock() + 1) * currentHeatmap.timeSquareScale - 1,
				(heatDiffSample1 - currentHeatmap.startBlock()) * currentHeatmap.timeSquareScale,
				(heatDiffSample2 - currentHeatmap.startBlock() + 1) * currentHeatmap.timeSquareScale - 1
			);
		} else {
			heatDiffSample1 = -1;
			heatDiffSample2 = -1;
			if (shiftPressed && heatActiveSample1 !== -1) {
				heatActiveSample2 = sample;
			} else {
				heatActiveSample1 = heatActiveSample2 = sample;
			}

			let minSelected = Math.min(heatActiveSample1, heatActiveSample2);
			let maxSelected = Math.max(heatActiveSample1, heatActiveSample2);

			callHighlightRedraw(highlightStart, highlightEnd, 'Diff');
			callHighlightRedraw(minSelected, maxSelected, 'Active');
			renderWrapper(
				(minSelected - currentHeatmap.startBlock()) * currentHeatmap.timeSquareScale,
				(maxSelected - currentHeatmap.startBlock() + 1) * currentHeatmap.timeSquareScale - 1
			);
		}
	}

	function updateSelectionArea(delta, endOnly, diff) {
		highlightStart = -1;
		highlightEnd = -1;
		if (diff) {
			if (heatDiffSample1 === -1) {
				let minSelected = Math.min(heatActiveSample1, heatActiveSample2);
				let maxSelected = Math.max(heatActiveSample1, heatActiveSample2);
				heatDiffSample1 = minSelected + delta;
				heatDiffSample2 = maxSelected + delta;
			} else {
				if (!endOnly) {
					heatDiffSample1 += delta;
				}
				heatDiffSample2 += delta;
			}
			let min = Math.min(heatDiffSample1, heatDiffSample2);
			let max = Math.max(heatDiffSample1, heatDiffSample2);
			callHighlightRedraw(min, max, 'Diff');
		} else {
			if (!endOnly) {
				heatActiveSample1 += delta;
			}
			heatActiveSample2 += delta;
			let minSelected = Math.min(heatActiveSample1, heatActiveSample2);
			let maxSelected = Math.max(heatActiveSample1, heatActiveSample2);

			callHighlightRedraw(minSelected, maxSelected, 'Active');
			callHighlightRedraw(highlightStart, highlightEnd, 'Selection');
			if (heatDiffSample1 === -1) {
				callHighlightRedraw(minSelected, maxSelected, 'Diff');
			}
		}
		let minSelected = Math.min(heatActiveSample1, heatActiveSample2);
		let maxSelected = Math.max(heatActiveSample1, heatActiveSample2);

		if (heatDiffSample1 === -1) {
			renderWrapper(
				(minSelected - currentHeatmap.startBlock()) * currentHeatmap.timeSquareScale,
				(maxSelected - currentHeatmap.startBlock() + 1) * currentHeatmap.timeSquareScale - 1
			);
		} else {
			let min = Math.min(heatDiffSample1, heatDiffSample2);
			let max = Math.max(heatDiffSample1, heatDiffSample2);
			renderWrapperDiff(
				(minSelected - currentHeatmap.startBlock()) * currentHeatmap.timeSquareScale,
				(maxSelected - currentHeatmap.startBlock() + 1) * currentHeatmap.timeSquareScale - 1,
				(min - currentHeatmap.startBlock()) * currentHeatmap.timeSquareScale,
				(max - currentHeatmap.startBlock() + 1) * currentHeatmap.timeSquareScale - 1
			);
		}
	}

	window.addEventListener('keydown', e => {
		if (e.key === 'Shift') {
			callSelectionRedraw(heatLastSample, true, e.ctrlKey);
		} else if (e.key === 'Control') {
			callSelectionRedraw(heatLastSample, e.shiftKey, true);
		}
	});

	window.addEventListener('keyup', e => {
		if (e.key === 'Shift') {
			callSelectionRedraw(heatLastSample, false, e.ctrlKey);
		} else if (e.key === 'Control') {
			callSelectionRedraw(heatLastSample, e.shiftKey, false);
		}
	});

	window.addEventListener('keydown', e => {
		switch (e.key) {
			case 'ArrowUp':
				updateSelectionArea(-1, e.shiftKey, e.ctrlKey);
				break;
			case 'ArrowDown':
				updateSelectionArea(+1, e.shiftKey, e.ctrlKey);
				break;
			case 'ArrowLeft':
				updateSelectionArea(-currentHeatmap.timeSquareRowsCount, e.shiftKey, e.ctrlKey);
				break;
			case 'ArrowRight':
				updateSelectionArea(+currentHeatmap.timeSquareRowsCount, e.shiftKey, e.ctrlKey);
				break;
			case '1':
				selectMode('flame-mode-flame');
				break;
			case '2':
				selectMode('flame-mode-reversed');
				break;
			case '3':
				selectMode('flame-mode-methods');
				break;
			case 'r':
			case 'R':
				if (!e.ctrlKey) {
					return;
				}
				if (heatDiffSample1 !== -1) {
					renderWrapperDiff(lastBaseRenderFrom, lastBaseRenderTo, lastRenderFrom, lastRenderTo);
				}
				break;
			case 'f':
			case 'F':
				if (!e.ctrlKey) {
					return;
				}
				search(true, e.shiftKey);
				break;
			case 'Escape':
				search(false);
				break;
			default:
				return;
		}
		e.preventDefault();
	});

	window.onkeydown = function (e) {
		if (e.keyCode === 16) {
			if (previewX !== -1 && previewY !== -1) {
				showPreview();
			}
		}
	}

	window.onkeyup = function (e) {
		if (!e.shiftKey) {
			preview_wrapper.style.display = 'none';
		}
	}

	let moved = false;
	heatCanvas.onmousemove = function (event) {
		heatCanvas.style.cursor = event.offsetY < canvasTimeHeightPx ? 'pointer' : '';
		let x = Math.floor((event.offsetX + heatCanvasContainer.scrollLeft - canvasScrollPadding) / sqPx);
		let y = Math.floor(Math.max(0, event.offsetY - canvasTimeHeightPx) / sqPx);
		if (y >= currentHeatmap.timeSquareRowsCount) {
			y = currentHeatmap.timeSquareRowsCount - 1;
		}
		heatLastSample = x * currentHeatmap.timeSquareRowsCount + y;
		if ((event.buttons & 1) === 1) {
			callSelectionRedraw(heatLastSample, true, event.ctrlKey);
		} else {
			callSelectionRedraw(heatLastSample, event.shiftKey, event.ctrlKey);
		}
		moved = true;
	}

	heatCanvas.onmouseout = function () {
		status.style.display = 'none';
	};

	heatCanvas.onmousedown = function (event) {
		if (event.offsetY < canvasTimeHeightPx) {
			moved = false;
			return;
		}

		let x = Math.floor((event.offsetX + heatCanvasContainer.scrollLeft - canvasScrollPadding) / sqPx);
		let y = Math.floor(Math.max(0, event.offsetY - canvasTimeHeightPx) / sqPx);
		let sample = x * currentHeatmap.timeSquareRowsCount + y;

		callActiveRedraw(sample, event.shiftKey, event.ctrlKey);
		moved = false;
	};

	heatCanvas.onmouseup = function (event) {
		if (moved) {
			let x = Math.floor((event.offsetX + heatCanvasContainer.scrollLeft - canvasScrollPadding) / sqPx);
			let y = Math.floor(Math.max(0, event.offsetY - canvasTimeHeightPx) / sqPx);
			let sample = x * currentHeatmap.timeSquareRowsCount + y;

			callActiveRedraw(sample, true, event.ctrlKey);
		}
	};

	heatCanvas.onclick = function (event) {
		if (event.offsetY < canvasTimeHeightPx && !moved) {
			currentTimezone = currentTimezone === 'Local' ? 'UTC' : 'Local';
			localStorage.setItem('heatmap-timezone', currentTimezone);
			redrawHeatMap();
		}
	};

	renderHeatmap();
	// first render of dominators
	renderFunc = renderTraces;
</script>
</body>
</html>
